mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-08-05 12:33:15 +00:00
feat(metrics): improve mempool and failed transaction metrics & panels
This commit is contained in:
parent
ab12fb104c
commit
849bb6bbe1
@ -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";
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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"
|
||||
}
|
||||
],
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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 min–max 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)]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user