Add sudo_unstable_metrics JSON-RPC function - #3318
Conversation
Add a metrics module with atomic counters/gauges: one NetworkMetrics per process and one ChainMetrics per chain, threaded through the network/sync/runtime/transactions/json-rpc services and incremented next to the existing log statements. Not exposed yet.
Returns a snapshot of the internal metric families as JSON. The list of metrics and their labels is unstable. Light-base only; full-node answers with the existing not-implemented error.
Add networkRequestSecondsTotal{protocol} count+sum next to the existing
request outcome counters. Duration is client-observed (includes
background-task queueing), measured in the NetworkServiceChain foreground
wrappers; the PhantomData marker becomes a real platform field to get
access to the clock.
syncWarpSyncHeight follows the last verified warp fragment; syncWarpSyncTargetHeight is the peer-claimed finalized height captured at the WarpAhead commit. Both read 0 until first set.
Set SMOLDOT_METRICS_OUT=<file.json> on run_chainhead_test.sh to poll sudo_unstable_metrics during the run (5s default) and render a self-contained HTML report (heights incl. warp progress, peers, request rates/durations, connection churn). The sampler polls the muxed chain through JsonRpcMux and rpc-managed chains through a new id-routed sendRpcOutOfBand, so it cannot steal the test's messages; the dump is written in a finally block so failing runs keep their graphs.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
Metrics example generated with below command: Returned json with snapshot {
"t": 1785492166420,
"chain": "para",
"metrics": [
{
"name": "networkConnectionsStartedTotal",
"type": "counter",
"entries": [
{
"value": 123
}
]
},
{
"name": "networkConnectionsHandshakesFinishedTotal",
"type": "counter",
"entries": [
{
"value": 22
}
]
},
{
"name": "networkConnectionsShutdownsTotal",
"type": "counter",
"entries": [
{
"value": 112
}
]
},
{
"name": "networkDiscoveryAddressesDroppedTotal",
"type": "counter",
"entries": [
{
"labels": {
"reason": "peer-id-mismatch"
},
"value": 0
},
{
"labels": {
"reason": "not-supported"
},
"value": 8
},
{
"labels": {
"reason": "invalid"
},
"value": 0
}
]
},
{
"name": "networkRequestsTotal",
"type": "counter",
"entries": [
{
"labels": {
"outcome": "success",
"protocol": "blocks"
},
"value": 5
},
{
"labels": {
"outcome": "failure",
"protocol": "blocks"
},
"value": 0
},
{
"labels": {
"outcome": "success",
"protocol": "warp-sync"
},
"value": 0
},
{
"labels": {
"outcome": "failure",
"protocol": "warp-sync"
},
"value": 0
},
{
"labels": {
"outcome": "success",
"protocol": "storage-proof"
},
"value": 1
},
{
"labels": {
"outcome": "failure",
"protocol": "storage-proof"
},
"value": 0
},
{
"labels": {
"outcome": "success",
"protocol": "call-proof"
},
"value": 2
},
{
"labels": {
"outcome": "failure",
"protocol": "call-proof"
},
"value": 0
}
]
},
{
"name": "networkRequestSecondsTotal",
"type": "counter",
"entries": [
{
"labels": {
"protocol": "blocks"
},
"value": 0.588458
},
{
"labels": {
"protocol": "warp-sync"
},
"value": 0
},
{
"labels": {
"protocol": "storage-proof"
},
"value": 1.112125
},
{
"labels": {
"protocol": "call-proof"
},
"value": 0.039935
}
]
},
{
"name": "networkPeerBansTotal",
"type": "counter",
"entries": [
{
"labels": {
"reason": "bad-block"
},
"value": 0
},
{
"labels": {
"reason": "bad-block-announce"
},
"value": 0
},
{
"labels": {
"reason": "bad-child-trie-root"
},
"value": 0
},
{
"labels": {
"reason": "bad-grandpa-commit"
},
"value": 0
},
{
"labels": {
"reason": "bad-justification"
},
"value": 0
},
{
"labels": {
"reason": "bad-merkle-proof"
},
"value": 0
},
{
"labels": {
"reason": "bad-warp-sync-fragment"
},
"value": 0
},
{
"labels": {
"reason": "invalid-call-proof"
},
"value": 0
},
{
"labels": {
"reason": "blocks-request-failed"
},
"value": 0
},
{
"labels": {
"reason": "call-proof-request-failed"
},
"value": 0
},
{
"labels": {
"reason": "child-storage-request-failed"
},
"value": 0
},
{
"labels": {
"reason": "storage-request-failed"
},
"value": 0
},
{
"labels": {
"reason": "warp-sync-request-failed"
},
"value": 0
}
]
},
{
"name": "networkGossipPeersConnected",
"type": "gauge",
"entries": [
{
"value": 4
}
]
},
{
"name": "syncBlocksVerifiedTotal",
"type": "counter",
"entries": [
{
"labels": {
"outcome": "success"
},
"value": 35
},
{
"labels": {
"outcome": "failure"
},
"value": 0
}
]
},
{
"name": "syncFinalityProofsVerifiedTotal",
"type": "counter",
"entries": [
{
"labels": {
"outcome": "success"
},
"value": 0
},
{
"labels": {
"outcome": "failure"
},
"value": 0
}
]
},
{
"name": "syncWarpFragmentsVerifiedTotal",
"type": "counter",
"entries": [
{
"value": 0
}
]
},
{
"name": "syncBestBlockHeight",
"type": "gauge",
"entries": [
{
"value": 19687690
}
]
},
{
"name": "syncFinalizedBlockHeight",
"type": "gauge",
"entries": [
{
"value": 19687672
}
]
},
{
"name": "syncWarpSyncHeight",
"type": "gauge",
"entries": [
{
"value": 0
}
]
},
{
"name": "syncWarpSyncTargetHeight",
"type": "gauge",
"entries": [
{
"value": 0
}
]
},
{
"name": "runtimeCompilationsTotal",
"type": "counter",
"entries": [
{
"value": 1
}
]
},
{
"name": "runtimeCompilationErrorsTotal",
"type": "counter",
"entries": [
{
"value": 0
}
]
},
{
"name": "runtimeCompilationSecondsTotal",
"type": "counter",
"entries": [
{
"value": 0.148646
}
]
},
{
"name": "runtimeCacheHitsTotal",
"type": "counter",
"entries": [
{
"value": 0
}
]
},
{
"name": "transactionsDroppedTotal",
"type": "counter",
"entries": [
{
"labels": {
"reason": "gap-in-chain"
},
"value": 0
},
{
"labels": {
"reason": "max-pending-transactions-reached"
},
"value": 0
},
{
"labels": {
"reason": "invalid"
},
"value": 0
},
{
"labels": {
"reason": "validate-error"
},
"value": 0
}
]
},
{
"name": "jsonrpcRequestsTotal",
"type": "counter",
"entries": [
{
"value": 75
}
]
}
]
} |
| /// Metrics of the network service. One instance per process, shared between all chains. | ||
| #[derive(Debug, Default)] | ||
| pub struct NetworkMetrics { | ||
| pub connections_started: Counter, |
There was a problem hiding this comment.
These are peers that connected on the webrtc and the connections_handshakes_finished represent the peers that completed the handshakes? 🤔
There was a problem hiding this comment.
These counters apply to any transport type (websocket, WebRTC, TCP).
connection_started is incremented upon each dial attempt, no matter the result.
connections_handshakes_finished is the subset of those that completed the handshake.
And the counters are per-connection, not per-peer - one peer can account for several attempts.
I'll update the doc comment.
Thanks!
| #[derive(Debug, Clone, strum::EnumDiscriminants)] | ||
| #[strum_discriminants(name(DropReasonKind))] | ||
| #[strum_discriminants(derive(strum::EnumIter, strum::IntoStaticStr))] | ||
| #[strum_discriminants(strum(serialize_all = "kebab-case"))] |
There was a problem hiding this comment.
This is for label info in grafana?
There was a problem hiding this comment.
Sort of - it's the reason label of the transactionsDroppedTotal metric in the sudo_unstable_metrics response (not Grafana-specific; anything consuming the metrics sees it, e.g. a Prometheus adapter or the e2e HTML report). EnumDiscriminants generates a payload-free DropReasonKind usable as a label, EnumIter gives the snapshot the full label set (zero counts included), IntoStaticStr + kebab-case turn variant names into the label strings, and the disabled variants (Finalized = success path, Crashed) are deliberately not counted. A test pins the exact label strings so a rename can't silently change the metric.
Added docs for this. Thanks!
| } | ||
|
|
||
| /// Returns the process-wide network metrics. | ||
| pub fn metrics(&self) -> &Arc<metrics::NetworkMetrics> { |
There was a problem hiding this comment.
do we actually need Arc here?
| "networkRequestSecondsTotal", { protocol: p }, | ||
| "networkRequestsTotal", { protocol: p, outcome: "success" }, | ||
| ), | ||
| })), |
There was a problem hiding this comment.
networkRequestSecondsTotal includes failures, but we only divide by success.
There was a problem hiding this comment.
Related code:
smoldot/light-base/src/metrics.rs
Lines 97 to 105 in 6fabafe
| rx.await.unwrap() | ||
| let result = rx.await.unwrap(); | ||
| self.metrics | ||
| .storage_proof_requests |
There was a problem hiding this comment.
child_storage_proof_request ? Or do we intentionally merge them?
| )] | ||
| #[strum(serialize_all = "kebab-case")] | ||
| pub enum BanReason { | ||
| BadBlock, |
|
|
||
| // 32-bit atomics so that the metrics also compile on targets without 64-bit | ||
| // atomics (e.g. `thumbv7m-none-eabi`); durations are tracked in milliseconds | ||
| // to make the range acceptable. |
There was a problem hiding this comment.
Nit: not sure if limiting to 32-bits on all platforms is OK here. Not saying it shall be 64bit - but maybe this deserves a second though if we can do something smarter here. E.g. 2^32 ~ 51 days, technically duration could accumulate to such number. But maybe it is ok.
michalkucharczyk
left a comment
There was a problem hiding this comment.
Skimmed the implementation, looks good. Left some nits.

Covers #3285
Adds
sudo_unstable_metrics, returning a snapshot of internal counters and gauges as a flat list of families (name,type,entrieswith labels). The set of metrics and labels is unstable and may change between versions.Covered: network connections and discovery drops (process-wide); per-chain request counts/durations per protocol, peer bans by reason, gossip peers, block/finality verification outcomes, best/finalized/warp heights, runtime compilations (including cold-start ones), dropped transactions, JSON-RPC request count.
Notes:
ban_and_disconnectnow takes aBanReasonenum instead of&'static str(network_serviceis public API).Use in e2e-tests
The chainhead e2e test can sample the new function during a run and render the time series as an HTML report:
This polls
sudo_unstable_metricson every chain every 5s (SMOLDOT_METRICS_INTERVAL_MSto change), writes the dump tometrics.json, and rendersmetrics.html(self-contained, no dependencies): block heights, warp progress, gossip peers, per-protocol request rate/failures/duration, runtime compilations, peer bans and discovery drops by reason, connection churn. The dump is written even when the test fails, so a bad run still produces a report - one paseo run with a warp stall was diagnosed straight from the charts (gossip peers at 0, handshake rate dropping to zero). Entirely opt-in: withoutSMOLDOT_METRICS_OUTnothing is sampled.