feat(metrics): improve mempool and failed transaction metrics & panels

This commit is contained in:
Daniil Polyakov 2026-07-28 17:06:18 +03:00
parent ab12fb104c
commit 849bb6bbe1
9 changed files with 427 additions and 83 deletions

View File

@ -1,6 +1,7 @@
pub const BLOCK_CREATION_TIME: &str = "block_creation_time";
pub const BLOCK_COUNT: &str = "block_count";
pub const MEMPOOL_SIZE: &str = "mempool_size";
pub const MEMPOOL_MAX_SIZE: &str = "mempool_max_size";
pub const MEMPOOL_TRANSACTION_APPLICATION_TIME: &str = "mempool_transaction_application_time";
pub const TRANSACTIONS_PER_BLOCK: &str = "transactions_per_block";
pub const FAILED_TRANSACTION_COUNT: &str = "failed_transaction_count";

View File

@ -51,6 +51,15 @@ pub fn record_mempool_size(size: usize) {
.set(u64::try_from(size).expect("Mempool size should fit into u64") as f64);
}
pub fn record_mempool_max_size(size: usize) {
gauge!(
description: "Configured maximum size of the mempool",
unit: Unit::Count,
names::MEMPOOL_MAX_SIZE
)
.set(u64::try_from(size).expect("Mempool max size should fit into u64") as f64);
}
pub fn record_mempool_transaction_application_time(
origin: TransactionOrigin,
kind: TxKind,

View File

@ -224,6 +224,7 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
let is_fresh_start = initial_checkpoint.is_none();
let (mempool, mempool_handle) = MemPool::new(config.mempool_max_size);
sequencer_core_metrics::record_mempool_max_size(config.mempool_max_size);
let block_publisher = BP::new(
&config.bedrock_config,

View File

@ -19,7 +19,7 @@
"targets": [
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"expr": "block_count_total",
"expr": "block_count",
"legendFormat": "height",
"refId": "A"
}
@ -42,7 +42,7 @@
"targets": [
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"expr": "rate(block_count_total[1m]) * 60",
"expr": "rate(block_count[1m]) * 60",
"legendFormat": "blocks/min",
"refId": "A"
}
@ -73,31 +73,31 @@
"targets": [
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"expr": "block_creation_time_seconds{quantile=\"0.5\"}",
"expr": "block_creation_time{quantile=\"0.5\"}",
"legendFormat": "p50",
"refId": "A"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"expr": "block_creation_time_seconds{quantile=\"0.9\"}",
"expr": "block_creation_time{quantile=\"0.9\"}",
"legendFormat": "p90",
"refId": "B"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"expr": "block_creation_time_seconds{quantile=\"0.95\"}",
"expr": "block_creation_time{quantile=\"0.95\"}",
"legendFormat": "p95",
"refId": "C"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"expr": "block_creation_time_seconds{quantile=\"0.99\"}",
"expr": "block_creation_time{quantile=\"0.99\"}",
"legendFormat": "p99",
"refId": "D"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"expr": "rate(block_creation_time_seconds_sum[1m]) / rate(block_creation_time_seconds_count[1m])",
"expr": "rate(block_creation_time_sum[1m]) / rate(block_creation_time_count[1m])",
"legendFormat": "avg",
"refId": "E"
}
@ -120,25 +120,25 @@
"targets": [
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"expr": "mempool_transaction_application_time_seconds{quantile=\"0.5\"}",
"expr": "mempool_transaction_application_time{quantile=\"0.5\"}",
"legendFormat": "p50 · {{kind}} · {{origin}}",
"refId": "A"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"expr": "mempool_transaction_application_time_seconds{quantile=\"0.9\"}",
"expr": "mempool_transaction_application_time{quantile=\"0.9\"}",
"legendFormat": "p90 · {{kind}} · {{origin}}",
"refId": "B"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"expr": "mempool_transaction_application_time_seconds{quantile=\"0.95\"}",
"expr": "mempool_transaction_application_time{quantile=\"0.95\"}",
"legendFormat": "p95 · {{kind}} · {{origin}}",
"refId": "C"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"expr": "mempool_transaction_application_time_seconds{quantile=\"0.99\"}",
"expr": "mempool_transaction_application_time{quantile=\"0.99\"}",
"legendFormat": "p99 · {{kind}} · {{origin}}",
"refId": "D"
}
@ -146,32 +146,6 @@
"title": "Transaction application time",
"type": "timeseries"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"custom": { "drawStyle": "line", "lineWidth": 1, "fillOpacity": 10, "spanNulls": true },
"unit": "short"
},
"overrides": [ ]
},
"gridPos": { "h": 9, "w": 12, "x": 12, "y": 16 },
"id": 5,
"options": {
"legend": { "displayMode": "list", "placement": "bottom", "calcs": [ "last", "max" ] },
"tooltip": { "mode": "single" }
},
"targets": [
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"expr": "mempool_size",
"legendFormat": "mempool size",
"refId": "A"
}
],
"title": "Mempool size",
"type": "timeseries"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
@ -186,8 +160,8 @@
}
]
},
"gridPos": { "h": 9, "w": 12, "x": 0, "y": 25 },
"id": 6,
"gridPos": { "h": 9, "w": 12, "x": 12, "y": 16 },
"id": 5,
"options": {
"legend": { "displayMode": "table", "placement": "bottom", "calcs": [ "last", "max" ] },
"tooltip": { "mode": "multi", "sort": "desc" }
@ -230,7 +204,130 @@
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": { "custom": { "drawStyle": "line", "lineWidth": 1, "fillOpacity": 10 }, "unit": "short" },
"defaults": {
"unit": "percent",
"decimals": 1,
"min": 0.0,
"max": 100.0,
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null },
{ "color": "orange", "value": 70.0 },
{ "color": "red", "value": 90.0 }
]
}
},
"overrides": [ ]
},
"gridPos": { "h": 8, "w": 6, "x": 0, "y": 25 },
"id": 6,
"options": {
"reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false },
"showThresholdLabels": false,
"showThresholdMarkers": true
},
"targets": [
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"expr": "100 * mempool_size / mempool_max_size",
"legendFormat": "utilization",
"refId": "A"
}
],
"title": "Mempool utilization",
"type": "gauge"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"custom": { "drawStyle": "line", "lineWidth": 1, "fillOpacity": 10, "spanNulls": true },
"unit": "short",
"min": 0.0
},
"overrides": [
{
"matcher": { "id": "byName", "options": "capacity" },
"properties": [
{ "id": "custom.lineStyle", "value": { "dash": [ 8, 4 ], "fill": "dash" } },
{ "id": "color", "value": { "mode": "fixed", "fixedColor": "red" } }
]
},
{
"matcher": { "id": "byName", "options": "queued" },
"properties": [ { "id": "color", "value": { "mode": "fixed", "fixedColor": "blue" } } ]
}
]
},
"gridPos": { "h": 8, "w": 18, "x": 6, "y": 25 },
"id": 7,
"options": {
"legend": { "displayMode": "table", "placement": "bottom", "calcs": [ "last", "max" ] },
"tooltip": { "mode": "multi", "sort": "desc" }
},
"targets": [
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"expr": "mempool_size",
"legendFormat": "queued",
"refId": "A"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"expr": "mempool_max_size",
"legendFormat": "capacity",
"refId": "B"
}
],
"title": "Mempool size vs capacity",
"type": "timeseries"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"unit": "percent",
"decimals": 2,
"min": 0.0,
"max": 100.0,
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null },
{ "color": "orange", "value": 1.0 },
{ "color": "red", "value": 5.0 }
]
}
},
"overrides": [ ]
},
"gridPos": { "h": 8, "w": 6, "x": 0, "y": 33 },
"id": 8,
"options": {
"reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false },
"showThresholdLabels": false,
"showThresholdMarkers": true
},
"targets": [
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"expr": "100 * increase(failed_transaction_count[$__range]) / clamp_min(increase(submitted_transaction_count[$__range]), 1)",
"legendFormat": "failed",
"refId": "A"
}
],
"title": "Failed transactions share",
"type": "gauge"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"custom": { "drawStyle": "line", "lineWidth": 1, "fillOpacity": 10 },
"unit": "short",
"min": 0.0
},
"overrides": [
{
"matcher": { "id": "byName", "options": "failed" },
@ -242,8 +339,8 @@
}
]
},
"gridPos": { "h": 9, "w": 12, "x": 12, "y": 25 },
"id": 7,
"gridPos": { "h": 8, "w": 18, "x": 6, "y": 33 },
"id": 9,
"options": {
"legend": { "displayMode": "table", "placement": "bottom", "calcs": [ "last", "max" ] },
"tooltip": { "mode": "multi", "sort": "desc" }
@ -251,18 +348,18 @@
"targets": [
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"expr": "rate(submitted_transaction_count_total[1m]) * 60",
"expr": "rate(submitted_transaction_count[1m]) * 60",
"legendFormat": "submitted",
"refId": "A"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"expr": "rate(failed_transaction_count_total[1m]) * 60",
"expr": "rate(failed_transaction_count[1m]) * 60",
"legendFormat": "failed",
"refId": "B"
}
],
"title": "Transaction throughput (per minute)",
"title": "Submitted vs failed transactions (per minute)",
"type": "timeseries"
}
],

View File

@ -10,8 +10,8 @@
)]
use dashboard_gen::{
Color, Dashboard, FieldOverride, Panel, Target, Unit, avg, percentiles, percentiles_labeled,
rate_per_min,
Color, Dashboard, FieldOverride, Panel, Target, Thresholds, Unit, avg, percentiles,
percentiles_labeled, rate_per_min,
};
use json_pretty_compact::PrettyCompactFormatter;
use serde::Serialize as _;
@ -68,19 +68,6 @@ fn sequencer_dashboard() -> Dashboard {
PERCENTILES,
" · {{kind}} · {{origin}}",
)),
Panel::timeseries("Mempool size")
.width(12)
.unit(Unit::Short)
.span_nulls()
.target(
Target::new(sequencer_core_metrics::names::MEMPOOL_SIZE)
.legend("mempool size"),
),
],
)
.row(
9,
[
Panel::timeseries("Transactions per block")
.width(12)
.unit(Unit::Short)
@ -94,9 +81,78 @@ fn sequencer_dashboard() -> Dashboard {
.dashed_line()
.color(Color::fixed("text")),
),
Panel::timeseries("Transaction throughput (per minute)")
.width(12)
],
)
.row(
8,
[
Panel::gauge("Mempool utilization")
.width(6)
.unit(Unit::Percent)
.decimals(1)
.min(0.0)
.max(100.0)
.thresholds(
Thresholds::base("green")
.step(70.0, "orange")
.step(90.0, "red"),
)
.target(
Target::new(format!(
"100 * {size} / {max_size}",
size = sequencer_core_metrics::names::MEMPOOL_SIZE,
max_size = sequencer_core_metrics::names::MEMPOOL_MAX_SIZE,
))
.legend("utilization"),
),
Panel::timeseries("Mempool size vs capacity")
.width(18)
.unit(Unit::Short)
.span_nulls()
.min(0.0)
.target(
Target::new(sequencer_core_metrics::names::MEMPOOL_SIZE).legend("queued"),
)
.target(
Target::new(sequencer_core_metrics::names::MEMPOOL_MAX_SIZE)
.legend("capacity"),
)
.with_override(
FieldOverride::by_name("capacity")
.dashed_line()
.color(Color::fixed("red")),
)
.with_override(FieldOverride::by_name("queued").color(Color::fixed("blue"))),
],
)
.row(
8,
[
Panel::gauge("Failed transactions share")
.width(6)
.unit(Unit::Percent)
.decimals(2)
.min(0.0)
.max(100.0)
.thresholds(
Thresholds::base("green")
.step(1.0, "orange")
.step(5.0, "red"),
)
.target(
Target::new(format!(
// `clamp_min` keeps an idle window (nothing submitted)
// reading as 0% instead of a division by zero.
"100 * increase({failed}[$__range]) / clamp_min(increase({submitted}[$__range]), 1)",
failed = sequencer_core_metrics::names::FAILED_TRANSACTION_COUNT,
submitted = sequencer_service_metrics::names::SUBMITTED_TRANSACTION_COUNT,
))
.legend("failed"),
),
Panel::timeseries("Submitted vs failed transactions (per minute)")
.width(18)
.unit(Unit::Short)
.min(0.0)
.target(rate_per_min(
sequencer_service_metrics::names::SUBMITTED_TRANSACTION_COUNT,
"submitted",

View File

@ -11,9 +11,10 @@ use std::fmt::Write as _;
use crate::{
Unit,
input::PanelInput,
input::{Defaults, PanelInput},
schema::{
AxisPlacement, Color, GradientMode, LineInterpolation, PanelType, ShowPoints, StackingMode,
ThresholdMode, Thresholds,
},
};
@ -33,25 +34,12 @@ 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;
// `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:?}))")?;
}
write_defaults(&mut expr, defaults)?;
if let Some(custom) = &defaults.custom {
if custom.span_nulls == Some(true) {
@ -146,6 +134,65 @@ fn panel_expr_inner(panel: &PanelInput) -> Result<String, std::fmt::Error> {
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",

View File

@ -14,6 +14,7 @@ use serde_json::Value;
use crate::schema::{
AxisPlacement, Color, GradientMode, LineInterpolation, PanelType, ShowPoints, StackingMode,
Thresholds,
};
#[derive(Deserialize)]
@ -54,6 +55,12 @@ pub struct Defaults {
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)]

View File

@ -14,12 +14,15 @@
// 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};
pub use schema::{
AxisPlacement, Color, GradientMode, LineInterpolation, ShowPoints, StackingMode, Thresholds,
};
use schema::{
Calc, Custom, Datasource, Defaults, DrawStyle, EmptyList, FieldConfig, Fill, GraphMode,
GridPos, Legend, LegendDisplay, LineStyle, Matcher, MatcherKind, Options, OverrideProperty,
PanelModel, PanelType, Placement, PropertyId, PropertyValue, ReduceOptions, SortOrder,
Stacking, StatColorMode, StatOptions, TimeRange, TimeSeriesOptions, Tooltip, TooltipMode,
Calc, Custom, Datasource, Defaults, DrawStyle, EmptyList, FieldConfig, Fill, GaugeOptions,
GraphMode, GridPos, Legend, LegendDisplay, LineStyle, Matcher, MatcherKind, Options,
OverrideProperty, PanelModel, PanelType, Placement, PropertyId, PropertyValue, ReduceOptions,
SortOrder, Stacking, StatColorMode, StatOptions, TimeRange, TimeSeriesOptions, Tooltip,
TooltipMode,
};
use serde::Serialize;
pub use unit::Unit;
@ -107,6 +110,7 @@ impl FieldOverride {
enum Kind {
Stat,
TimeSeries,
Gauge,
}
/// A dashboard panel builder. Grid position and panel id are assigned by
@ -119,6 +123,9 @@ pub struct Panel {
unit: Option<Unit>,
decimals: Option<u32>,
color: Option<Color>,
min: Option<f64>,
max: Option<f64>,
thresholds: Option<Thresholds>,
span_nulls: bool,
overrides: Vec<FieldOverride>,
// Optional timeseries styling, set via the `styling` setters.
@ -140,6 +147,9 @@ impl Panel {
unit: None,
decimals: None,
color: None,
min: None,
max: None,
thresholds: None,
span_nulls: false,
overrides: Vec::new(),
line_interpolation: None,
@ -161,6 +171,12 @@ impl Panel {
Self::new(title, Kind::TimeSeries)
}
/// A radial gauge panel. Pair it with [`Panel::min`]/[`Panel::max`] — the
/// dial needs a range to fill — and [`Panel::thresholds`] for its coloring.
pub fn gauge(title: impl Into<String>) -> Self {
Self::new(title, Kind::Gauge)
}
/// Grid width in Grafana's 24-column units. Unset panels split the row's
/// remaining width evenly.
#[must_use]
@ -187,6 +203,26 @@ impl Panel {
self
}
/// Lower bound of the value scale.
#[must_use]
pub const fn min(mut self, min: f64) -> Self {
self.min = Some(min);
self
}
/// Upper bound of the value scale.
#[must_use]
pub const fn max(mut self, max: f64) -> Self {
self.max = Some(max);
self
}
#[must_use]
pub fn thresholds(mut self, thresholds: Thresholds) -> Self {
self.thresholds = Some(thresholds);
self
}
#[must_use]
pub const fn span_nulls(mut self) -> Self {
self.span_nulls = true;
@ -230,6 +266,9 @@ impl Panel {
custom: None,
unit,
decimals: self.decimals,
min: self.min,
max: self.max,
thresholds: self.thresholds,
};
let options = Options::Stat(StatOptions {
color_mode: StatColorMode::Value,
@ -242,6 +281,27 @@ impl Panel {
});
(defaults, options, PanelType::Stat)
}
Kind::Gauge => {
let defaults = Defaults {
color: self.color,
custom: None,
unit,
decimals: self.decimals,
min: self.min,
max: self.max,
thresholds: self.thresholds,
};
let options = Options::Gauge(GaugeOptions {
reduce_options: ReduceOptions {
calcs: vec![Calc::LastNotNull],
fields: String::new(),
values: false,
},
show_threshold_labels: false,
show_threshold_markers: true,
});
(defaults, options, PanelType::Gauge)
}
Kind::TimeSeries => {
let defaults = Defaults {
color: None,
@ -262,6 +322,9 @@ impl Panel {
}),
unit,
decimals: None,
min: self.min,
max: self.max,
thresholds: self.thresholds,
};
// Panels with several series read better as a sortable table with
// a multi-series tooltip; single-series panels stay compact.

View File

@ -94,6 +94,54 @@ pub enum SortOrder {
pub enum PanelType {
Stat,
Timeseries,
Gauge,
}
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ThresholdMode {
Absolute,
/// Steps expressed as a percentage of the minmax range. The builder never
/// emits this; it exists so Grafana exports using it still parse.
Percentage,
}
/// 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)]
pub struct ThresholdStep {
pub color: String,
pub value: Option<f64>,
}
/// A threshold ladder, driving gauge/stat coloring.
#[derive(Clone, Serialize, Deserialize)]
pub struct Thresholds {
pub mode: ThresholdMode,
pub steps: Vec<ThresholdStep>,
}
impl Thresholds {
/// Start a ladder with the color used below every threshold.
pub fn base(color: impl Into<String>) -> Self {
Self {
mode: ThresholdMode::Absolute,
steps: vec![ThresholdStep {
color: color.into(),
value: None,
}],
}
}
/// Add a step: values at or above `value` render in `color`.
#[must_use]
pub fn step(mut self, value: f64, color: impl Into<String>) -> Self {
self.steps.push(ThresholdStep {
color: color.into(),
value: Some(value),
});
self
}
}
// Optional timeseries styling vocabularies. Each derives `PartialEq` so the
@ -248,6 +296,12 @@ pub struct Defaults {
pub unit: Unit,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub decimals: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thresholds: Option<Thresholds>,
}
#[derive(Serialize)]
@ -293,11 +347,20 @@ pub struct TimeSeriesOptions {
pub tooltip: Tooltip,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GaugeOptions {
pub reduce_options: ReduceOptions,
pub show_threshold_labels: bool,
pub show_threshold_markers: bool,
}
#[derive(Serialize)]
#[serde(untagged)]
pub enum Options {
Stat(StatOptions),
TimeSeries(TimeSeriesOptions),
Gauge(GaugeOptions),
}
#[derive(Clone, Copy, Serialize)]