From ec53979442fae50c3b75b5cae892e19d0e05bd45 Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Tue, 28 Jul 2026 02:15:43 +0300 Subject: [PATCH] feat(dashboard_gen): make unit an enum --- .../src/bin/gen_sequencer_dashboard.rs | 16 +-- tools/dashboard_gen/src/codegen.rs | 7 +- tools/dashboard_gen/src/lib.rs | 12 +- tools/dashboard_gen/src/schema.rs | 4 +- tools/dashboard_gen/src/unit.rs | 107 ++++++++++++++++++ 5 files changed, 130 insertions(+), 16 deletions(-) create mode 100644 tools/dashboard_gen/src/unit.rs diff --git a/tools/dashboard_gen/src/bin/gen_sequencer_dashboard.rs b/tools/dashboard_gen/src/bin/gen_sequencer_dashboard.rs index 8c1a9321..4e0716ab 100644 --- a/tools/dashboard_gen/src/bin/gen_sequencer_dashboard.rs +++ b/tools/dashboard_gen/src/bin/gen_sequencer_dashboard.rs @@ -10,7 +10,7 @@ )] use dashboard_gen::{ - Color, Dashboard, FieldOverride, Panel, Target, avg, percentiles, percentiles_labeled, + Color, Dashboard, FieldOverride, Panel, Target, Unit, avg, percentiles, percentiles_labeled, rate_per_min, }; use json_pretty_compact::PrettyCompactFormatter; @@ -26,7 +26,7 @@ fn sequencer_dashboard() -> Dashboard { [ Panel::stat("Chain height") .width(6) - .unit("short") + .unit(Unit::Short) .decimals(0) .color(Color::fixed("blue")) .target( @@ -34,7 +34,7 @@ fn sequencer_dashboard() -> Dashboard { ), Panel::timeseries("Block production rate") .width(18) - .unit("short") + .unit(Unit::Short) .target(rate_per_min( sequencer_core_metrics::names::BLOCK_COUNT, "blocks/min", @@ -45,7 +45,7 @@ fn sequencer_dashboard() -> Dashboard { 9, [Panel::timeseries("Block creation time") .width(24) - .unit("s") + .unit(Unit::Seconds) .targets(percentiles( sequencer_core_metrics::names::BLOCK_CREATION_TIME, PERCENTILES, @@ -62,7 +62,7 @@ fn sequencer_dashboard() -> Dashboard { [ Panel::timeseries("Transaction application time") .width(12) - .unit("s") + .unit(Unit::Seconds) .targets(percentiles_labeled( sequencer_core_metrics::names::MEMPOOL_TRANSACTION_APPLICATION_TIME, PERCENTILES, @@ -70,7 +70,7 @@ fn sequencer_dashboard() -> Dashboard { )), Panel::timeseries("Mempool size") .width(12) - .unit("short") + .unit(Unit::Short) .span_nulls() .target( Target::new(sequencer_core_metrics::names::MEMPOOL_SIZE) @@ -83,7 +83,7 @@ fn sequencer_dashboard() -> Dashboard { [ Panel::timeseries("Transactions per block") .width(12) - .unit("short") + .unit(Unit::Short) .targets(percentiles( sequencer_core_metrics::names::TRANSACTIONS_PER_BLOCK, PERCENTILES, @@ -96,7 +96,7 @@ fn sequencer_dashboard() -> Dashboard { ), Panel::timeseries("Transaction throughput (per minute)") .width(12) - .unit("short") + .unit(Unit::Short) .target(rate_per_min( sequencer_service_metrics::names::SUBMITTED_TRANSACTION_COUNT, "submitted", diff --git a/tools/dashboard_gen/src/codegen.rs b/tools/dashboard_gen/src/codegen.rs index efa8d8c3..15f79cb4 100644 --- a/tools/dashboard_gen/src/codegen.rs +++ b/tools/dashboard_gen/src/codegen.rs @@ -10,6 +10,7 @@ use std::fmt::Write as _; use crate::{ + Unit, input::PanelInput, schema::{ AxisPlacement, Color, GradientMode, LineInterpolation, PanelType, ShowPoints, StackingMode, @@ -38,7 +39,11 @@ fn panel_expr_inner(panel: &PanelInput) -> Result { let defaults = &panel.field_config.defaults; // `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:?})")?; + write!( + expr, + "\n .unit({})", + Unit::from_id(unit).to_rust_source() + )?; } if let Some(decimals) = defaults.decimals { write!(expr, "\n .decimals({decimals})")?; diff --git a/tools/dashboard_gen/src/lib.rs b/tools/dashboard_gen/src/lib.rs index 243b22cb..918061a0 100644 --- a/tools/dashboard_gen/src/lib.rs +++ b/tools/dashboard_gen/src/lib.rs @@ -22,11 +22,13 @@ use schema::{ Stacking, StatColorMode, StatOptions, TimeRange, TimeSeriesOptions, Tooltip, TooltipMode, }; use serde::Serialize; +pub use unit::Unit; mod codegen; mod input; mod schema; mod styling; +mod unit; /// Datasource uid every panel/target points at. Dashboards stay portable across /// environments because they reference the datasource by this stable uid rather @@ -114,7 +116,7 @@ pub struct Panel { kind: Kind, targets: Vec, width: u32, - unit: Option, + unit: Option, decimals: Option, color: Option, span_nulls: bool, @@ -168,8 +170,8 @@ impl Panel { } #[must_use] - pub fn unit(mut self, unit: impl Into) -> Self { - self.unit = Some(unit.into()); + pub fn unit(mut self, unit: Unit) -> Self { + self.unit = Some(unit); self } @@ -439,8 +441,8 @@ pub fn rate_per_min(metric: &str, legend: &str) -> Target { Target::new(format!("rate({metric}[1m]) * 60")).legend(legend) } -fn default_unit() -> String { - "short".to_owned() +const fn default_unit() -> Unit { + Unit::Short } fn ref_letter(index: usize) -> String { diff --git a/tools/dashboard_gen/src/schema.rs b/tools/dashboard_gen/src/schema.rs index 7b6138d8..88d24bd8 100644 --- a/tools/dashboard_gen/src/schema.rs +++ b/tools/dashboard_gen/src/schema.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; -use crate::{DATASOURCE_UID, FieldOverride, Target}; +use crate::{DATASOURCE_UID, FieldOverride, Target, unit::Unit}; #[derive(Clone, Copy, Serialize)] #[serde(rename_all = "lowercase")] @@ -245,7 +245,7 @@ pub struct Defaults { pub color: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub custom: Option, - pub unit: String, + pub unit: Unit, #[serde(default, skip_serializing_if = "Option::is_none")] pub decimals: Option, } diff --git a/tools/dashboard_gen/src/unit.rs b/tools/dashboard_gen/src/unit.rs new file mode 100644 index 00000000..adeebf3b --- /dev/null +++ b/tools/dashboard_gen/src/unit.rs @@ -0,0 +1,107 @@ +//! Panel value units. +//! +//! Grafana identifies a field's unit by a short id string (`"s"`, `"bytes"`, +//! `"reqps"`, …). We model the handful we actually use as named variants and +//! fall back to [`Unit::custom`] for anything else, so the value always +//! serializes to the exact id Grafana expects. + +use serde::{Serialize, Serializer}; + +/// A panel value unit. Serializes to Grafana's unit id string. +/// +/// Only the most common units are named; [`Unit::custom`] carries any other +/// Grafana unit id verbatim. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Unit { + /// Plain number, SI-abbreviated (`short`). Grafana's default. + Short, + /// Percentage on a 0–100 scale (`percent`). + Percent, + /// Percentage on a 0.0–1.0 scale (`percentunit`). + PercentUnit, + /// Seconds (`s`). + Seconds, + /// Milliseconds (`ms`). + Milliseconds, + /// Nanoseconds (`ns`). + Nanoseconds, + /// Bytes, IEC/binary (`bytes`). + Bytes, + /// Bytes per second, SI (`Bps`). + BytesPerSec, + /// Requests per second (`reqps`). + RequestsPerSec, + /// Operations per second (`ops`). + OpsPerSec, + /// Any other Grafana unit id, kept verbatim. + Custom(String), +} + +impl Unit { + /// Wrap an arbitrary Grafana unit id (e.g. `"dtdurationms"`, `"celsius"`). + #[must_use] + pub fn custom(id: impl Into) -> Self { + Self::Custom(id.into()) + } + + /// The Grafana unit id this value serializes to. + fn as_id(&self) -> &str { + match self { + Self::Short => "short", + Self::Percent => "percent", + Self::PercentUnit => "percentunit", + Self::Seconds => "s", + Self::Milliseconds => "ms", + Self::Nanoseconds => "ns", + Self::Bytes => "bytes", + Self::BytesPerSec => "Bps", + Self::RequestsPerSec => "reqps", + Self::OpsPerSec => "ops", + 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 { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_str(self.as_id()) + } +}