decdn_node/metrics.rs
1//! OpenMetrics/Prometheus metrics and a minimal `/metrics` HTTP server.
2//!
3//! Metrics live in an [`iroh_metrics::Registry`] so we can surface both our
4//! `decdn_*` counters and iroh's own transport metrics through a single
5//! endpoint. Output is `OpenMetrics` text, which Prometheus scrapers accept.
6
7use std::collections::{BTreeMap, BTreeSet};
8use std::net::SocketAddr;
9use std::sync::{Arc, Mutex, OnceLock, RwLock};
10use std::time::{Duration, Instant};
11
12use alloy::primitives::U256;
13use bytes::Bytes;
14use decdn_cache::{CacheEngine, CacheMetrics};
15use decdn_incentive::PoolOpenFailureReason;
16use decdn_protocol::Region;
17use http_body_util::Full;
18use hyper::service::service_fn;
19use hyper::{Request, Response, StatusCode};
20use hyper_util::rt::TokioIo;
21use iroh::Endpoint;
22use iroh::metrics::EndpointMetrics;
23use iroh_metrics::{
24 Counter, EncodeLabelSet, EncodeLabelValue, Family, Gauge, Histogram, MetricsGroup,
25 MetricsSource, Registry,
26};
27use serde::{Deserialize, Serialize};
28use tokio::net::TcpListener;
29use tokio::sync::{Semaphore, oneshot};
30
31use crate::chain_events::resumable_watcher::WatcherHook;
32use crate::load_shed::RequestClass;
33
34/// Bucket upper bounds of `decdn_probe_collection_latency_seconds`, per the
35/// registry row in `adr/appendix-observability.md`. The collection window has a
36/// 500 ms ceiling, so the top finite bucket leaves room for a slow scheduler.
37const PROBE_COLLECTION_BUCKETS: [f64; 8] = [0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5];
38
39/// Bucket upper bounds shared by the time-to-first-byte histograms. A cache hit
40/// resolves in the low buckets. A serve miss includes discovery and an upstream
41/// fill — the whole blob, on a buffered fill — and a pull leg includes its open
42/// and a stall window, so the top bucket reaches past the outer pull deadline at
43/// the default timeouts (`selection::outer_pull_deadline`). A value in `+Inf`
44/// alone pins `histogram_quantile` at the top finite bound.
45const FIRST_BYTE_BUCKETS: [f64; 15] = [
46 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0, 120.0, 300.0,
47];
48
49/// Bucket upper bounds of `decdn_rpc_request_duration_seconds`. The top finite
50/// bucket is the node's per-call RPC deadline
51/// ([`crate::chain_events::DEFAULT_RPC_CALL_TIMEOUT`]): a request that reaches
52/// the deadline is dropped and counts as `timeout` without a duration sample.
53const RPC_REQUEST_BUCKETS: [f64; 10] = [0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0];
54
55/// Cap concurrent `/metrics` connections. Prevents a trivial `DoS` where a
56/// peer opens many sockets to the operational-data endpoint and exhausts
57/// tasks.
58const MAX_METRICS_CONNECTIONS: usize = 32;
59
60/// Every counter that names why an inbound `cdn/client/v1` stream failed. Each
61/// failed inbound stream counts on exactly one of them, so
62/// `decdn_streams_failed_total{direction="inbound"}` minus their sum is zero. The
63/// serve handler counts every end in one place, so a scrape never sees a failure
64/// before its reason. The "Unattributed stream failures" panels of the reference
65/// dashboards in `decdn/devops` subtract this list by hand, so a new reason here
66/// needs the same edit there.
67pub const INBOUND_FAILURE_REASONS: &[&str] = &[
68 "decdn_serve_stream_rejected_bad_binding_total",
69 "decdn_serve_stream_rejected_blob_too_large_total",
70 "decdn_serve_stream_rejected_cache_miss_total",
71 "decdn_serve_stream_rejected_chain_hash_denied_total",
72 "decdn_serve_stream_rejected_chain_stale_total",
73 "decdn_serve_stream_rejected_evicted_since_probe_total",
74 "decdn_serve_stream_rejected_foreign_declined_total",
75 "decdn_serve_stream_rejected_hash_denied_total",
76 "decdn_serve_stream_rejected_insufficient_deposit_total",
77 "decdn_serve_stream_rejected_internal_error_total",
78 "decdn_serve_stream_rejected_load_shed_hit_total",
79 "decdn_serve_stream_rejected_load_shed_miss_total",
80 "decdn_serve_stream_rejected_origin_denied_total",
81 "decdn_serve_stream_rejected_owner_mismatch_total",
82 "decdn_serve_stream_rejected_pool_closing_total",
83 "decdn_serve_stream_rejected_pool_unconfirmed_total",
84 "decdn_serve_stream_rejected_pull_loop_guard_total",
85 "decdn_serve_stream_rejected_range_not_satisfiable_total",
86 "decdn_serve_stream_rejected_signer_cap_exhausted_total",
87 "decdn_serve_stream_rejected_signer_floor_at_cap_total",
88 "decdn_serve_stream_rejected_stream_cap_full_total",
89 "decdn_serve_stream_rejected_unknown_lane_total",
90 "decdn_serve_stream_request_unreadable_total",
91 "decdn_serve_stream_midstream_pool_exhausted_total",
92 "decdn_serve_stream_midstream_signer_cap_exhausted_total",
93 "decdn_serve_stream_terminated_takedown_total",
94 "decdn_serve_stream_voucher_rejected_total",
95 "decdn_serve_stream_proof_budget_exhausted_total",
96 "decdn_serve_stream_client_declined_total",
97 "decdn_serve_stream_client_abandoned_total",
98 "decdn_serve_stream_node_fault_total",
99];
100
101#[derive(
102 Debug,
103 Clone,
104 Copy,
105 Hash,
106 PartialEq,
107 Eq,
108 PartialOrd,
109 Ord,
110 Serialize,
111 Deserialize,
112 EncodeLabelValue,
113)]
114enum StreamDirection {
115 Inbound,
116 Outbound,
117}
118
119#[derive(
120 Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, EncodeLabelSet,
121)]
122struct StreamLabels {
123 direction: StreamDirection,
124}
125
126/// Why a probe for a present blob got **no** eviction hold, as the `reason`
127/// label on `decdn_probe_hold_unavailable_total`.
128///
129/// Holds are best-effort, so this is not the same as "answered
130/// `has_blob: false`" — `exhausted` and `stake_lane_reserved` still advertise.
131///
132/// The three values are not interchangeable — each has a different operator
133/// remedy, which is why the reference `DecdnProbeHoldViolations` alert filters
134/// on `reason="exhausted"` alone. The derive renders variants in `snake_case`, so
135/// these encode as `reason="exhausted"` / `"disabled"` / `"stake_lane_reserved"`.
136///
137/// Not to be confused with [`decdn_cache::ProbeHoldOutcome::Unavailable`],
138/// which is the one probe-hold outcome that emits **no** metric at all — a
139/// blob genuinely absent is a true negative, not a refusal. The shared word is
140/// inverted between the two types: here it selects *for* the counter, there it
141/// selects *against* it.
142#[derive(
143 Debug,
144 Clone,
145 Copy,
146 Hash,
147 PartialEq,
148 Eq,
149 PartialOrd,
150 Ord,
151 Serialize,
152 Deserialize,
153 EncodeLabelValue,
154)]
155pub enum ProbeHoldUnavailableReason {
156 /// Blob present, but **all** hold slots were live (`max_probe_holds`
157 /// reached) — genuine budget pressure. The node still advertises
158 /// `has_blob: true` and forgoes only the hold, so the blob may be
159 /// LRU-evicted before the pull lands. This is the "raise `max_probe_holds`"
160 /// signal and the only value the `DecdnProbeHoldViolations` alert fires on.
161 Exhausted,
162 /// Blob present, but the eviction-hold path is **disabled by config**
163 /// (`max_probe_holds == 0`) — an intentional operator choice, not budget
164 /// pressure (#739). The one reason that also suppresses the advertisement:
165 /// the node answers `has_blob: false` for store-backed content (origin-held
166 /// content takes no hold and is unaffected). Raising `max_probe_holds` is
167 /// the remedy only if the disable was unintended; alerting on it would be
168 /// nonsensical.
169 Disabled,
170 /// An end-client probe (a requester that is *not* a registered operator)
171 /// hit the stake-lane-reserved end-client ceiling
172 /// (`max_probe_holds - cache.stake_lane_reserved_holds`), keeping hold
173 /// headroom for node-to-node cache-miss probes per ADR 003 §Admission and
174 /// Priority (#757). The reservation is a content-independent *admission*
175 /// decision, so unlike the two above it is decided before any hold attempt
176 /// — but the handler still consults the cache afterwards to answer
177 /// honestly, and advertises the blob if it is present. Zero unless
178 /// `cache.stake_lane_reserved_holds > 0`.
179 StakeLaneReserved,
180}
181
182impl ProbeHoldUnavailableReason {
183 /// Every variant, in the order [`Metrics::new`] materializes them.
184 ///
185 /// This is what makes "every reason exports at zero" structural rather
186 /// than a convention. [`Metrics::new`] destructures `ALL.map(…)` into its
187 /// three cached handles, so adding a variant here changes the array length
188 /// and **fails to compile** at that pattern — you cannot add a reason
189 /// without materializing its child. The recorder's exhaustive `match` is
190 /// the second gate; without this array a fourth variant could satisfy the
191 /// compiler with a `_ =>` arm calling `get_or_create`, silently
192 /// reintroducing the lazily-created series this design exists to prevent.
193 ///
194 /// Order is load-bearing (it binds handles positionally); a reorder is
195 /// caught by `probe_hold_unavailable_increments_only_the_named_reason`.
196 const ALL: [Self; 3] = [Self::Exhausted, Self::Disabled, Self::StakeLaneReserved];
197}
198
199#[derive(
200 Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, EncodeLabelSet,
201)]
202struct ProbeHoldUnavailableLabels {
203 reason: ProbeHoldUnavailableReason,
204}
205
206/// The `node_region` label on `decdn_staker_set_active_by_region`.
207///
208/// The label is `node_region`, not `region`: every reference dashboard scopes
209/// its selectors on a scrape-side `region` target label. Under the default
210/// `honor_labels: false`, Prometheus renames a clashing metric label to
211/// `exported_region`, so the metric's own label would be lost.
212#[derive(
213 Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, EncodeLabelSet,
214)]
215struct NodeRegionLabels {
216 node_region: String,
217}
218
219/// The `method` label on `decdn_rpc_requests_total` and
220/// `decdn_rpc_request_duration_seconds`: the JSON-RPC method of one request
221/// the node's alloy providers sent.
222///
223/// A closed set keeps the label's cardinality fixed. The variants are the
224/// methods the node's reads, fillers and settlement path call; any other
225/// method name counts as [`Self::Other`], so a request never mints a series
226/// from a string it did not choose. Values render as the JSON-RPC method name
227/// (`eth_getLogs`), not in `snake_case`.
228#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
229pub(crate) enum RpcMethod {
230 EthBlockNumber,
231 EthCall,
232 EthChainId,
233 EthEstimateGas,
234 EthFeeHistory,
235 EthGasPrice,
236 EthGetBalance,
237 EthGetBlockByNumber,
238 EthGetCode,
239 EthGetLogs,
240 EthGetTransactionByHash,
241 EthGetTransactionCount,
242 EthGetTransactionReceipt,
243 EthMaxPriorityFeePerGas,
244 EthSendRawTransaction,
245 Other,
246}
247
248impl RpcMethod {
249 /// Every variant. [`Metrics::new`] materializes one child per entry, so
250 /// each series exports at zero from startup.
251 pub(crate) const ALL: [Self; 16] = [
252 Self::EthBlockNumber,
253 Self::EthCall,
254 Self::EthChainId,
255 Self::EthEstimateGas,
256 Self::EthFeeHistory,
257 Self::EthGasPrice,
258 Self::EthGetBalance,
259 Self::EthGetBlockByNumber,
260 Self::EthGetCode,
261 Self::EthGetLogs,
262 Self::EthGetTransactionByHash,
263 Self::EthGetTransactionCount,
264 Self::EthGetTransactionReceipt,
265 Self::EthMaxPriorityFeePerGas,
266 Self::EthSendRawTransaction,
267 Self::Other,
268 ];
269
270 /// The label value: the JSON-RPC method name, or `other`.
271 pub(crate) const fn as_str(self) -> &'static str {
272 match self {
273 Self::EthBlockNumber => "eth_blockNumber",
274 Self::EthCall => "eth_call",
275 Self::EthChainId => "eth_chainId",
276 Self::EthEstimateGas => "eth_estimateGas",
277 Self::EthFeeHistory => "eth_feeHistory",
278 Self::EthGasPrice => "eth_gasPrice",
279 Self::EthGetBalance => "eth_getBalance",
280 Self::EthGetBlockByNumber => "eth_getBlockByNumber",
281 Self::EthGetCode => "eth_getCode",
282 Self::EthGetLogs => "eth_getLogs",
283 Self::EthGetTransactionByHash => "eth_getTransactionByHash",
284 Self::EthGetTransactionCount => "eth_getTransactionCount",
285 Self::EthGetTransactionReceipt => "eth_getTransactionReceipt",
286 Self::EthMaxPriorityFeePerGas => "eth_maxPriorityFeePerGas",
287 Self::EthSendRawTransaction => "eth_sendRawTransaction",
288 Self::Other => "other",
289 }
290 }
291
292 /// Map a request's method name onto the closed set. An unlisted name,
293 /// including the literal `other`, is [`Self::Other`].
294 pub(crate) fn from_name(name: &str) -> Self {
295 Self::ALL
296 .into_iter()
297 .find(|method| *method != Self::Other && method.as_str() == name)
298 .unwrap_or(Self::Other)
299 }
300}
301
302impl EncodeLabelValue for RpcMethod {
303 fn encode_label_value(&self) -> iroh_metrics::LabelValue<'_> {
304 iroh_metrics::LabelValue::Str(std::borrow::Cow::Borrowed(self.as_str()))
305 }
306}
307
308/// The `outcome` label on `decdn_rpc_requests_total`: how one JSON-RPC
309/// request ended. Each request counts under exactly one value. The derive
310/// renders the variants in `snake_case`.
311#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, EncodeLabelValue)]
312pub(crate) enum RpcOutcome {
313 /// The provider answered with a result.
314 Ok,
315 /// The provider answered with a revert: JSON-RPC error code `3`, or a
316 /// message that names a revert. The provider worked; the call itself
317 /// failed, so this is not a provider fault.
318 Reverted,
319 /// The provider answered with any other JSON-RPC error response, such as
320 /// invalid params, a range cap on `eth_getLogs`, or a provider-side
321 /// internal error.
322 RpcError,
323 /// The provider throttled the request: HTTP 429, an HTTP error with a
324 /// `Retry-After` delay, or a JSON-RPC error with code 429 or rate-limit
325 /// wording.
326 RateLimited,
327 /// No usable JSON-RPC reply arrived: a refused or reset connection, an HTTP
328 /// error without a JSON-RPC body, an unreadable body, or a batch reply
329 /// that left this request out.
330 TransportError,
331 /// The request did not resolve in time: the HTTP client timed out, or the
332 /// caller dropped the request before a reply arrived. In this node the
333 /// caller drops a request when its per-call deadline fires.
334 Timeout,
335}
336
337impl RpcOutcome {
338 /// Every variant. [`Metrics::new`] materializes one child per
339 /// `(method, outcome)` pair, so each series exports at zero from startup.
340 pub(crate) const ALL: [Self; 6] = [
341 Self::Ok,
342 Self::Reverted,
343 Self::RpcError,
344 Self::RateLimited,
345 Self::TransportError,
346 Self::Timeout,
347 ];
348}
349
350#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, EncodeLabelSet)]
351struct RpcRequestLabels {
352 method: RpcMethod,
353 outcome: RpcOutcome,
354}
355
356#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, EncodeLabelSet)]
357struct RpcMethodLabels {
358 method: RpcMethod,
359}
360
361/// Which buy-ceiling regime an ADR 041 serve-economics refusal was decided
362/// under, dispatched to the matching sibling counter by
363/// [`Metrics::serve_economics_refused`].
364///
365/// Not a metric label: per `adr/appendix-observability.md` § Reason splits the
366/// convention is sibling counters, and the two regimes have distinct operator
367/// remedies. `Warming` means the source still had allowance, so the ceiling was
368/// the market price `max(sell, amortized)` and the market itself is above it —
369/// the market is too hot to relay into. `Amortized` means the source's warming
370/// allowance is spent (or warming is off), so the ceiling had already dropped
371/// to the grief-proof floor — the node's own sell rate or margin config is too
372/// tight. The regime is also emitted as a structured `debug!` field at the
373/// refusal site so a single event carries the candidate rate and ceiling.
374#[derive(Debug, Clone, Copy, PartialEq, Eq)]
375pub enum ServeEconomicsRegime {
376 /// The source had warming allowance: the ceiling was the market price and
377 /// the market is above it.
378 Warming,
379 /// The source's warming allowance is spent (or warming is off): the ceiling
380 /// was the amortized floor and this node's serve economics are too tight.
381 Amortized,
382}
383
384/// Build a [`WatcherHook`] that invokes one `&self` recorder on a shared
385/// `Metrics`, deduping the per-watcher `Box::new(move || metrics.foo())`
386/// closures each watcher site would otherwise define (#1251). All
387/// six `LogSink` sites route their liveness (`*_tick`) and panic
388/// (`*_task_panicked`) hooks — and their down-family established/backoff edges —
389/// through this helper (blacklist composes it with its readiness-gate closures).
390/// Pass the recorder as a method path,
391/// e.g. `metric_hook(&metrics, Metrics::slash_watcher_cycle_established)`.
392pub(crate) fn metric_hook(metrics: &Arc<Metrics>, record: fn(&Metrics)) -> WatcherHook {
393 let metrics = Arc::clone(metrics);
394 Box::new(move || record(&metrics))
395}
396
397/// deCDN-specific counters and gauges surfaced at `/metrics`.
398///
399/// The group name becomes the metric-name prefix, so fields appear as
400/// e.g. `decdn_probe_requests_total`.
401#[derive(Debug, Serialize, Deserialize, MetricsGroup)]
402#[metrics(default, name = "decdn")]
403pub struct DecdnMetrics {
404 /// Total probe requests served.
405 pub probe_requests: Counter,
406 /// Currently open QUIC connections.
407 pub active_connections: Gauge,
408 /// Currently open paid delivery streams, split by node direction.
409 streams_active: Family<StreamLabels, Gauge>,
410 /// Currently open inbound serve lanes (distinct `(pool, signer, provider)` keys).
411 pub lanes_open: Gauge,
412 /// Raw USDC still recoverable from the pools currently paying this node:
413 /// `Σ (deposit − totalRedeemed)` over the distinct pools the redeemer
414 /// planned lanes against, refreshed once per redeemer self-tick beside
415 /// [`Self::unredeemed_usdc`]. Already-redeemed funds have left the pool, so
416 /// this is the ceiling those pools can still pay — not their lifetime
417 /// deposits. Operator-visible name: `decdn_pool_deposit_usdc`.
418 pub pool_deposit_usdc: Gauge,
419 /// Raw USDC in this node's own buyer wallet — what it can still escrow as a
420 /// deposit when it opens a payment pool on its cache-miss leg.
421 ///
422 /// The buyer leg is the one part of the node that spends rather than earns,
423 /// and nothing else reports on it: an operator who never funds this wallet
424 /// sees a node that serves perfectly and silently buys nothing, because
425 /// every `openPool` reverts on the ERC-20 transfer.
426 ///
427 /// Read at bootstrap and once per reclaim sweep (`RECLAIM_SWEEP_INTERVAL`),
428 /// so it lags a spend — or an operator top-up, which is the reading that
429 /// matters — by up to that long.
430 ///
431 /// Zero is meaningful only on a node with
432 /// `cache.node_to_node_pull_through_enabled` on; a cache-only node never
433 /// opens a pool and has no reason to hold USDC.
434 pub buyer_wallet_usdc: Gauge,
435 /// `decdn_buyer_lane_seed_failures_total`: this node could not establish a
436 /// lane's already-paid watermark, so it refused the pull rather than
437 /// resuming the lane from zero.
438 ///
439 /// A lane resumed from zero is stranded permanently, not for one pull: the
440 /// pull persists its own progress on every exit path, which gives the lane a
441 /// local row and stops the reseed ever running for that provider again. The
442 /// node refuses instead, and this counts how often. A sustained rate means
443 /// the chain lane or the buyer store is unhealthy and this node is buying
444 /// nothing from the affected providers.
445 pub buyer_lane_seed_failures: Counter,
446 /// `decdn_buyer_pool_adoption_failures_total`: bootstrap could not tell
447 /// whether this node already owns a payment pool on chain, so it left the
448 /// first cache miss to open one.
449 ///
450 /// Every increment is a chance that the node escrows a second deposit beside
451 /// one it already holds. Adoption runs once per process, so this cannot
452 /// self-correct before the next restart. Pair with
453 /// `decdn_buyer_wallet_usdc`: the two together are what distinguishes "no
454 /// pool to adopt" from "could not look".
455 pub buyer_pool_adoption_failures: Counter,
456 /// Total raw USDC this node holds in accepted vouchers that it has not yet
457 /// redeemed on-chain — the sum of `owed − paid` across every inbound lane
458 /// the redeemer plans to collect. Refreshed once per redeemer self-tick
459 /// (`redeem_interval_secs`), not on every voucher, so it lags live accrual
460 /// by up to one interval. Operator-visible name: `decdn_unredeemed_usdc`.
461 pub unredeemed_usdc: Gauge,
462 /// `cdn/client/v1` connections closed by the application-layer idle reaper
463 /// (ADR 005 §Connection lifetime): no stream for `APP_IDLE_TIMEOUT` after the
464 /// last one closed. A sustained rate flags peers parking streamless
465 /// keep-alive'd connections — the abuse pattern the reaper exists to reclaim,
466 /// invisible in `active_connections` alone. Operator-visible name:
467 /// `decdn_client_idle_close_total`.
468 pub client_idle_close: Counter,
469 /// OTLP span-export batches whose export call failed (connect error,
470 /// non-OK gRPC status, or timeout), counted at the batch exporter. Spans
471 /// the batch queue drops when full, and spans a collector rejects inside
472 /// an OK partial-success reply, are not counted: `0` does not mean no
473 /// traces were lost. Stays `0` when `observability.otlp_endpoint` is unset
474 /// or the node emits no spans. Operator-visible name:
475 /// `decdn_otlp_export_failures_total`.
476 pub otlp_export_failures: Counter,
477 /// Seconds since node start.
478 pub node_uptime_seconds: Gauge,
479 /// JSON-RPC endpoint reachability per the watchdog task. `1` =
480 /// reachable, `0` = unreachable. `adr/appendix-observability.md` names operational gauges in
481 /// the `decdn_*` family; this is the per-tick mirror of the startup
482 /// `check_rpc_reachability` probe so dashboards/alerts can fire on
483 /// a sustained outage rather than relying on a one-shot startup line.
484 pub rpc_healthy: Gauge,
485 /// Connections rejected because the global concurrency semaphore was
486 /// exhausted. Field has no `_total` suffix because the `OpenMetrics`
487 /// encoder appends it automatically; the operator-visible name is
488 /// `decdn_dispatch_rejected_global_total`.
489 pub dispatch_rejected_global: Counter,
490 /// Connections rejected by the per-source rate limiter. Operator-
491 /// visible name: `decdn_dispatch_rejected_per_source_total`.
492 pub dispatch_rejected_per_source: Counter,
493 /// Currently in-flight QUIC handler tasks holding a dispatch permit.
494 pub dispatch_in_flight: Gauge,
495 /// Connections accepted on a relay-only path (no resolvable peer
496 /// IP) while the per-source layer was enabled. The per-source rate
497 /// limit cannot be enforced for these — operators chasing
498 /// `dispatch_rejected_per_source` anomalies need this counter to
499 /// distinguish "the layer didn't fire" from "the layer wasn't
500 /// applicable." Operator-visible name:
501 /// `decdn_dispatch_per_source_skipped_no_addr_total`.
502 pub dispatch_per_source_skipped_no_addr: Counter,
503 /// Inbound connections (all ALPNs) that arrived with a direct IP path.
504 /// Operator-visible name: `decdn_inbound_connections_direct_total`.
505 pub inbound_connections_direct: Counter,
506 /// Inbound connections (all ALPNs) that arrived over the relay only, with
507 /// no direct IP path at accept time. Expected on a node behind NAT; on a
508 /// node with a public address it means dialers did not find or reach that
509 /// address. Operator-visible name:
510 /// `decdn_inbound_connections_relayed_total`.
511 pub inbound_connections_relayed: Counter,
512 /// `1` when the host's default route has a public source address (ADR 001
513 /// § Node Discovery), `0` when the node is behind NAT. Sampled at
514 /// bring-up. Operator-visible name: `decdn_node_public_address`.
515 pub node_public_address: Gauge,
516 /// `decdn_probe_hold_unavailable_total{reason}` per the canonical metric
517 /// registry (`adr/appendix-observability.md` — the authoritative naming
518 /// source, superseding informal ADR-005 references): a probe for a present
519 /// blob that got no eviction hold, broken out by cause on the `reason`
520 /// label (#1443, collapsing the former `probe_hold_violations` /
521 /// `probe_holds_disabled` / `probe_stake_lane_reserved` counters).
522 ///
523 /// For `exhausted` / `stake_lane_reserved` the node still advertises
524 /// (`has_blob: true`) and simply forgoes the hold — the blob may be
525 /// LRU-evicted before the pull, costing one wasted round trip. That is not
526 /// a slash (ADR 014 pairs no offense with a miss) and not a reputation
527 /// penalty either: a `NotFound` is classified `Transient` and suppresses
528 /// the (peer, hash) pair without scoring the peer. `disabled` is the
529 /// operator opt-out (`max_probe_holds == 0`) that answers `has_blob: false`
530 /// for store-backed content. See
531 /// [`ProbeHoldUnavailableReason`] for what each value means and which one
532 /// the "raise `max_probe_holds`" alert fires on; all three children are
533 /// materialized at startup by [`Metrics::new`] so each series is exported
534 /// at zero rather than appearing only on first increment. Field has no
535 /// `_total` suffix because the `OpenMetrics` encoder appends it.
536 ///
537 /// **This is the documented exception, not the convention** (#1475) — the
538 /// one labeled *reason split*, not the crate's only `Family`:
539 /// `streams_active` and `staker_set_active_by_region` label other axes.
540 /// Every other reason-style split in this crate — `dispatch_rejected_*`, `probe_rate_limit_rejected_*`,
541 /// and `pool_open_failures_*` — fans out to sibling unlabeled counters,
542 /// and that stays the default for a new split: sibling counters need no
543 /// `EncodeLabelSet` type, no pre-materialization to keep a series
544 /// exporting at zero, and no alert rewrite when a reason is added.
545 ///
546 /// The exception is earned because the three values share one *aggregate*
547 /// and one budget axis: an operator asks "are probe holds unavailable?"
548 /// first and drills into which value second — the shape a label serves and
549 /// sibling counters make awkward. They pointedly do NOT share an alert:
550 /// `DecdnProbeHoldViolations` filters to `reason="exhausted"` precisely
551 /// because [`ProbeHoldUnavailableReason::Disabled`] is an intentional
552 /// operator choice and alerting on it would be nonsensical, and
553 /// `StakeLaneReserved` has its own knob. Being able to express that filter
554 /// is itself part of what the label buys. A split whose values have
555 /// unrelated remedies *and* no meaningful aggregate gains nothing from a
556 /// label and should stay siblings.
557 probe_hold_unavailable: Family<ProbeHoldUnavailableLabels, Counter>,
558 /// `decdn_probe_hold_slots_used` (registry): current active
559 /// probe-triggered eviction holds (distinct held blobs), ADR 005
560 /// §Probe-triggered eviction hold. Sampled from the cache engine at
561 /// scrape time, so it tracks hold expiry with no probe traffic; pair
562 /// with `probe_hold_slots_max` for a saturation ratio.
563 pub probe_hold_slots_used: Gauge,
564 /// `decdn_probe_hold_slots_max` (registry, mandatory): the configured
565 /// `max_probe_holds` budget. Set once at startup. Pairs with
566 /// `probe_hold_slots_used` so dashboards can alert on a saturation
567 /// ratio rather than an absolute count.
568 pub probe_hold_slots_max: Gauge,
569 /// `cdn/dht/v1` requests rejected by the per-peer (`NodeId`) token
570 /// bucket (ADR 022 §DHT Rate Limiting). One Counter per layer to match
571 /// the existing `dispatch_rejected_*` convention: a plain counter field
572 /// carries no label dimension (a labeled series would need a `Family`). Operator-visible name:
573 /// `decdn_dht_rate_limit_rejected_per_peer_total`.
574 pub dht_rate_limit_rejected_per_peer: Counter,
575 /// `cdn/dht/v1` requests rejected by the per-IP token bucket. Sibling
576 /// to `dht_rate_limit_rejected_per_peer` — see its docs. Operator-
577 /// visible name: `decdn_dht_rate_limit_rejected_per_ip_total`.
578 pub dht_rate_limit_rejected_per_ip: Counter,
579 /// `cdn/dht/v1` requests rejected by the global token bucket. Sibling
580 /// to `dht_rate_limit_rejected_per_peer` — see its docs. Operator-
581 /// visible name: `decdn_dht_rate_limit_rejected_global_total`.
582 pub dht_rate_limit_rejected_global: Counter,
583 /// `retain_recent` sweeps of the per-IP DHT keyed-limiter map that
584 /// actually ran (single-flight CAS won, layer enabled). Bump from
585 /// both the lazy-prune path in `DhtRateLimiter::check` and the
586 /// periodic `gc_per_ip` GC task. Persistent growth without a matching
587 /// drop in `decdn_dht_rate_limit_tracked_per_ip` means the limiter
588 /// is observing churn faster than `retain_recent` can release entries
589 /// — investigate cap saturation. Operator-visible name:
590 /// `decdn_dht_rate_limit_prune_sweeps_per_ip_total` (#645).
591 pub dht_rate_limit_prune_sweeps_per_ip: Counter,
592 /// Sibling of `dht_rate_limit_prune_sweeps_per_ip` for the per-peer
593 /// (`NodeId`) keyed-limiter map. Operator-visible name:
594 /// `decdn_dht_rate_limit_prune_sweeps_per_peer_total` (#645).
595 pub dht_rate_limit_prune_sweeps_per_peer: Counter,
596 /// Current size of the per-IP DHT keyed-limiter map after the most
597 /// recent prune (lazy or periodic). Pair with
598 /// `decdn_dht_rate_limit_prune_sweeps_per_ip_total` to detect cap
599 /// saturation. Updated post-prune so the value is at most one
600 /// `DHT_RATE_LIMIT_GC_INTERVAL` stale on quiet nodes; reads `0`
601 /// until the first successful prune (lazy or periodic) after
602 /// startup. Operator-visible name:
603 /// `decdn_dht_rate_limit_tracked_per_ip` (#645).
604 pub dht_rate_limit_tracked_per_ip: Gauge,
605 /// Sibling of `dht_rate_limit_tracked_per_ip` for the per-peer map.
606 /// Operator-visible name: `decdn_dht_rate_limit_tracked_per_peer` (#645).
607 pub dht_rate_limit_tracked_per_peer: Gauge,
608 /// `cdn/probe/v1` requests rejected by the per-peer (`NodeId`) token
609 /// bucket (ADR 005 §Probe rate limiting). One Counter per layer to match
610 /// the existing `dht_rate_limit_rejected_*` / `dispatch_rejected_*`
611 /// convention; a plain counter field carries no label dimension (a labeled
612 /// series would need a `Family`);
613 /// operators recover the rolled-up rate with
614 /// `sum(rate({__name__=~"decdn_probe_rate_limit_rejected_(per_peer|per_ip|global)_total"}[1m]))`.
615 /// Operator-visible name: `decdn_probe_rate_limit_rejected_per_peer_total`.
616 pub probe_rate_limit_rejected_per_peer: Counter,
617 /// `cdn/probe/v1` requests rejected by the per-IP token bucket. Sibling
618 /// to `probe_rate_limit_rejected_per_peer` — see its docs. Operator-
619 /// visible name: `decdn_probe_rate_limit_rejected_per_ip_total`.
620 pub probe_rate_limit_rejected_per_ip: Counter,
621 /// `cdn/probe/v1` requests rejected by the global token bucket. Sibling
622 /// to `probe_rate_limit_rejected_per_peer` — see its docs. Operator-
623 /// visible name: `decdn_probe_rate_limit_rejected_global_total`.
624 pub probe_rate_limit_rejected_global: Counter,
625 /// `retain_recent` sweeps of the per-IP probe keyed-limiter map that
626 /// actually ran (single-flight CAS won, layer enabled). Bumped from both
627 /// the lazy-prune path in the limiter's `check` and the periodic
628 /// `gc_per_ip` GC task. Operator-visible name:
629 /// `decdn_probe_rate_limit_prune_sweeps_per_ip_total` (#645).
630 pub probe_rate_limit_prune_sweeps_per_ip: Counter,
631 /// Sibling of `probe_rate_limit_prune_sweeps_per_ip` for the per-peer
632 /// (`NodeId`) keyed-limiter map. Operator-visible name:
633 /// `decdn_probe_rate_limit_prune_sweeps_per_peer_total` (#645).
634 pub probe_rate_limit_prune_sweeps_per_peer: Counter,
635 /// Current size of the per-IP probe keyed-limiter map after the most
636 /// recent prune (lazy or periodic). Pair with
637 /// `decdn_probe_rate_limit_prune_sweeps_per_ip_total` to detect cap
638 /// saturation. Operator-visible name:
639 /// `decdn_probe_rate_limit_tracked_per_ip` (#645).
640 pub probe_rate_limit_tracked_per_ip: Gauge,
641 /// Sibling of `probe_rate_limit_tracked_per_ip` for the per-peer map.
642 /// Operator-visible name: `decdn_probe_rate_limit_tracked_per_peer` (#645).
643 pub probe_rate_limit_tracked_per_peer: Gauge,
644 /// `cdn/dht/v1` request handling failed after the request was admitted
645 /// by the rate limiter — frame decode error, response write error,
646 /// read timeout, etc. Tracked separately from the rate-limit
647 /// rejections so an operator running with default `RUST_LOG=info` can
648 /// see the rate of "I accepted this request and then it broke"
649 /// failures without scraping debug-level logs. Operator-visible name:
650 /// `decdn_dht_requests_failed_total`.
651 pub dht_requests_failed: Counter,
652 /// `Store` rejected because `holder != authenticated NodeId` (ADR
653 /// 022 §STORE Flow line 140). This is the lying-`holder` attack
654 /// signal — a non-zero value means at least one peer is trying to
655 /// publish records on behalf of someone else's `NodeId`. Operator-
656 /// visible name: `decdn_dht_store_rejected_holder_mismatch_total`.
657 pub dht_store_rejected_holder_mismatch: Counter,
658 /// `Store` rejected because the holder is not in the active-staker
659 /// set (ADR 022 §STORE Flow line 140). Sustained growth from many
660 /// distinct holders without matching `decdn_dht_store_accepted`
661 /// growth is a Sybil-attempt indicator. Operator-visible name:
662 /// `decdn_dht_store_rejected_non_staked_total`.
663 pub dht_store_rejected_non_staked: Counter,
664 /// `Store` rejected because the publisher is at the per-publisher
665 /// quota (ADR 022 §Content Records and TTL — 200-record hard cap).
666 /// Normal load shouldn't trip this. Operator-visible name:
667 /// `decdn_dht_store_rejected_quota_total`.
668 pub dht_store_rejected_quota: Counter,
669 /// `Store` admitted to the record store (newly inserted OR
670 /// refreshed). Pair with the three `dht_store_rejected_*` counters
671 /// to compute admission rate. Operator-visible name:
672 /// `decdn_dht_store_accepted_total`.
673 pub dht_store_accepted: Counter,
674 /// `BatchStore` requests that passed stage-1 rate limiting and the
675 /// batch-level `holder == authenticated NodeId` check, i.e. reached
676 /// per-hash admission (ADR 022 §STORE Flow Batched STORE, #648). The
677 /// per-hash outcomes still land in the shared `dht_store_*` counters
678 /// above, so this counts batches, not hashes — pair the two to see
679 /// average admitted batch size and batch adoption. Operator-visible
680 /// name: `decdn_dht_batch_store_received_total`.
681 pub dht_batch_store_received: Counter,
682 /// Hashes in a `BatchStore` acked `false` purely because the
683 /// two-stage rate-limit budget was exhausted before reaching them
684 /// (the `n - k` tail, ADR 022 §Batch token accounting / AC 18) — NOT
685 /// counted as a per-hash `Store` rejection because they were never
686 /// processed. Sustained growth means publishers are sending batches
687 /// larger than the per-peer burst allows in one window; the publisher
688 /// retries the tail. Operator-visible name:
689 /// `decdn_dht_batch_store_hashes_deferred_rate_limit_total`.
690 pub dht_batch_store_hashes_deferred_rate_limit: Counter,
691 /// Times the republisher lost its cache-commit event window — the
692 /// `subscribe_inserts` broadcast channel overflowed and reported
693 /// `Lagged`. Counts lag *events*, not distinct store walks: a lag
694 /// arriving while a sweep runs is folded into that sweep's next pass,
695 /// so this can outrun the number of walks performed. The cache does not
696 /// retain the missed hashes, so without a sweep a blob committed inside
697 /// the window stays undiscoverable until the node restarts (ADR 022
698 /// §Bootstrap). A nonzero rate means commits are outrunning the
699 /// scheduler; discovery still converges, but up to one cold-start
700 /// window late. Operator-visible name:
701 /// `decdn_dht_republish_lag_sweeps_total`.
702 pub dht_republish_lag_sweeps: Counter,
703
704 /// Lag events folded into a sweep that was already running or already
705 /// queued, rather than starting a walk of their own. The sibling that splits
706 /// `decdn_dht_republish_lag_sweeps_total` into the lags that claimed an idle
707 /// slot and the lags that did not.
708 ///
709 /// **Not** a walk count, and the difference is not one either: the slot has
710 /// a single queued position, so any number of lags arriving behind a running
711 /// worker all count here and collapse into one further pass. The ratio is
712 /// what reads: coalesced climbing at the lag rate, with the difference flat,
713 /// means the slot is never released. Operator-visible name:
714 /// `decdn_dht_republish_lag_sweeps_coalesced_total`.
715 pub dht_republish_lag_sweeps_coalesced: Counter,
716
717 /// Lag-sweep walks that died in a panic instead of completing. The walk
718 /// runs in its own task, so a panic releases the slot through the normal
719 /// path and a queued request still runs — but the dead pass seeded only
720 /// what it reached before it died. A rate here with
721 /// `decdn_dht_republish_sweep_reseeded_total` flat means every sweep dies
722 /// before repairing anything, which nothing else surfaces.
723 /// Operator-visible name:
724 /// `decdn_dht_republish_lag_sweep_panics_total`.
725 pub dht_republish_lag_sweep_panics: Counter,
726 /// Hashes a lag sweep newly scheduled for republish. Seeding is
727 /// idempotent, so this counts the repair, not the walk: a sweep that
728 /// finds every held hash already scheduled adds zero. Operator-visible
729 /// name: `decdn_dht_republish_sweep_reseeded_total`.
730 pub dht_republish_sweep_reseeded: Counter,
731 /// Bulk republish seeds that could not walk the local blob store and
732 /// covered only the origin-held half — both the boot-time cold start
733 /// and the lag sweep, which share one derivation. The seed degrades
734 /// rather than failing, so nothing else surfaces this: blobs held only
735 /// in the store stay un-republished until a later sweep re-walks the
736 /// store successfully, or until a restart. A boot-path failure is not
737 /// permanent — the next lag sweep recovers it — but nothing schedules
738 /// one, so a node with no further lag stays degraded for its lifetime.
739 /// Operator-visible name:
740 /// `decdn_dht_republish_seed_store_walk_failures_total`.
741 pub dht_republish_seed_store_walk_failures: Counter,
742
743 /// Stored hashes a bulk republish seed could not put to its origin — the
744 /// ownership test an origin-only node applies to every blob in its store,
745 /// answered by neither "yours" nor "not yours".
746 ///
747 /// The origin-side twin of
748 /// `decdn_dht_republish_seed_store_walk_failures_total`, and it degrades the
749 /// same way: a hash that cannot be confirmed is left out of the announce set
750 /// rather than advertised, because announcing on a transport blip risks
751 /// offering content the serve gate then refuses. So an unreachable remote
752 /// origin quietly shrinks the announce set of a node that holds the content.
753 /// Distinct from `decdn_cache_origin_probe_failures_total`, which counts the
754 /// rescan's probes rather than the seed's. Operator-visible name:
755 /// `decdn_dht_republish_seed_origin_probe_failures_total`.
756 pub dht_republish_seed_origin_probe_failures: Counter,
757 /// Accepted vouchers whose nonce skipped one or more values past the
758 /// previously-accepted nonce (`voucher.nonce > last_nonce + 1`), counted
759 /// once per gapped voucher (#747). The voucher is still accepted —
760 /// vouchers are cumulative, so on-chain settlement is unaffected — but a
761 /// non-zero rate flags dropped vouchers (per-voucher deliveries the node
762 /// never billed) or a client resetting/forking its counter (a replay-probe
763 /// signal). The precise skipped-count rides the paired `tracing::warn!` in
764 /// `ChannelState::apply_voucher`. Operator-visible name:
765 /// `decdn_voucher_nonce_gaps_total`.
766 pub voucher_nonce_gaps: Counter,
767 /// Redeem hints the voucher-accept path could not enqueue because the
768 /// bounded advisory channel was full (`try_send` → `Full`), counted once
769 /// per dropped hint (#751). A hint is advisory — the next voucher re-hints
770 /// and the redeemer self-tick / shutdown close still redeem — so a few drops
771 /// are benign, but a sustained non-zero rate means the redeemer is not
772 /// keeping up with channel fan-out and threshold redemption is leaning on
773 /// the slow self-tick. Operator-visible name:
774 /// `decdn_redeem_hints_dropped_total`.
775 pub redeem_hints_dropped: Counter,
776 /// Redeem hints the redeemer dropped because their lane is parked (#2340).
777 /// A lane parks when its hint-path redemption hits a chain fault — a
778 /// failed pre-redeem watermark read or a `redeemMany` that did not land,
779 /// an RPC fault or a revert alike. Each sweep (self-tick or serve cutoff)
780 /// retries the parked lanes with every other lane, and only a sweep that
781 /// finishes without a chain fault releases them. So an RPC outage costs one
782 /// failed attempt per lane per interval: the sweep's batched retry. Hints
783 /// for a parked lane stay dropped until a sweep finishes without a chain
784 /// fault. A non-zero rate tracks chain faults on the settlement path; read it beside
785 /// `decdn_onchain_tx_send_failed_total` and
786 /// `decdn_redemption_reconcile_failures_total`. Operator-visible name:
787 /// `decdn_redeem_hints_parked_total`.
788 pub redeem_hints_parked: Counter,
789 /// Download-receipt audit writes dropped because the bounded writer queue
790 /// was full (`try_send` → `Full`, #803), counted once per dropped receipt.
791 /// The receipt log is audit-only and the payment already advanced the lane
792 /// watermark, so a drop never affects settlement — but a sustained non-zero
793 /// rate means the receipt writer cannot keep up with disk I/O (a slow or full
794 /// `data_dir`, the end-state of #802) and audit/dispute records are being
795 /// lost. Operator-visible name:
796 /// `decdn_receipt_writes_dropped_total`.
797 pub receipt_writes_dropped: Counter,
798 /// ADR 041 warming serve credits dropped before they reached the ledger,
799 /// counted once per dropped credit. Either the bounded aggregator queue was
800 /// full (`try_send` → `Full`) or the aggregator was gone (`Closed`). A drop
801 /// is conservative — it leaves the source's ledger lower than reality,
802 /// never higher — so it can only block speculative warming,
803 /// never over-fund it. But a sustained non-zero rate means warming is being
804 /// throttled by lost bookkeeping rather than by real losses, and a rate that
805 /// tracks the serve rate exactly means the aggregator task is gone and every
806 /// source will drift to blocked. Operator-visible name:
807 /// `decdn_warming_credits_dropped_total`.
808 pub warming_credits_dropped: Counter,
809 /// ADR 041 warming serve credits the aggregator applied to a source ledger.
810 /// The success sibling of `warming_credits_dropped`, and the reason a zero
811 /// drop count means something: zero drops beside zero applies is a node
812 /// whose serve path never reached the ledger at all — an unwired
813 /// [`crate::warming_allowance::WarmingCreditSink`] leaves the inert
814 /// `NoopWarmingCreditSink` in place, which drops nothing because it enqueues
815 /// nothing. A node serving tagged blobs must show this climbing.
816 /// Operator-visible name: `decdn_warming_credits_applied_total`.
817 pub warming_credits_applied: Counter,
818 /// Redemption steps that failed: a `redeemMany` chunk that reverted
819 /// on-chain or that the RPC refused at `send`, a lane whose plan failed,
820 /// or a hint-path lane that could not be read from the lane store. A
821 /// chunk whose receipt stays unconfirmed after the by-hash fetch does not
822 /// count here; it counts into `decdn_onchain_tx_receipt_failed_total` or
823 /// `decdn_onchain_tx_timeout_total`. Each failure is otherwise only a
824 /// single `warn!`; a sustained rate means accrued earnings are not being
825 /// redeemed and warrants investigating the RPC / wallet. Operator-visible
826 /// name: `decdn_redemption_failures_total`.
827 pub redemption_failures: Counter,
828 /// Lanes the redeemer held or dropped for the pool's chain-observed
829 /// solvency (`pool_is_redeemable`, ADR 003): a drained `Open` pool is held
830 /// for a top-up, and a drained `Closing` pool, or one whose close deadline
831 /// falls within the redeemer's landing slack, is dropped.
832 /// Counted once per skipped lane per planning pass. A sustained non-zero
833 /// rate means real unredeemed value is stuck behind pools this node can no
834 /// longer collect from — worth checking against `pool_deposit_usdc` for
835 /// which pools are dry. Operator-visible name:
836 /// `decdn_redemption_skipped_insolvent_total`.
837 pub redemption_skipped_insolvent: Counter,
838 /// Lanes with value owed that the redeemer skipped because the signer's
839 /// capability has expired, or expires within the redeemer's landing slack
840 /// (ADR 003 §Revocation). The contract pays 0 for an expired capability, so
841 /// the redemption would only spend gas. Counted once per skipped lane per
842 /// planning pass. A lane with nothing owed is not counted. The serve path
843 /// stops accepting vouchers one redeem interval plus that slack before
844 /// expiry, so a sustained rate means a lane earned near its capability's
845 /// expiry and no sweep redeemed it in time: check for failed or
846 /// floor-deferred redemptions. Operator-visible name:
847 /// `decdn_redemption_skipped_expired_total`.
848 pub redemption_skipped_expired: Counter,
849 /// Lanes dropped from a redeem batch by the pre-submit on-chain watermark
850 /// reconciliation because the chain already shows them settled to their
851 /// claim value — a `redeemMany` the contract would silently no-op, caught
852 /// before it costs gas. Counted once per dropped lane. A sustained non-zero
853 /// rate means the event-fed paid cache is lagging the chain (or a second
854 /// actor is redeeming the same lanes); a burst right after a restart is the
855 /// expected catch-up. Operator-visible name:
856 /// `decdn_redemption_reconciled_skip_total`.
857 pub redemption_reconciled_skip: Counter,
858 /// Pre-submit on-chain watermark batches that landed and were reconciled
859 /// against the chain. Counted once per `getWatermarks` batch that decoded,
860 /// so a redeem sweep over more lanes than the read batch caps contributes
861 /// one per batch. This is the *attempt* signal
862 /// `redemption_reconciled_skip` cannot give: that counter is legitimately
863 /// zero on a healthy sweep with nothing to drop, so zero alone cannot
864 /// separate "the reconciliation ran and found nothing" from "the
865 /// reconciliation never ran". Read the two together — a redeemer planning
866 /// lanes with this flat at zero is not reconciling at all. Operator-visible
867 /// name: `decdn_redemption_reconcile_ok_total`.
868 pub redemption_reconcile_ok: Counter,
869 /// Pre-submit on-chain watermark batches that did not land — an RPC error,
870 /// a `timed` timeout, or a return whose length did not match the batch.
871 /// Counted once per failed batch. The read is fail-open (the contract's own
872 /// `claimed <= w.amount` guard is the backstop), so a sustained rate costs
873 /// gas rather than correctness: the seller submits `redeemMany` batches it
874 /// never checked against the chain. A sustained non-zero rate means the
875 /// chain RPC is rejecting or timing out the batch read — check the
876 /// endpoint's `eth_call` response-size and gas ceilings against the read
877 /// batch size. Operator-visible name:
878 /// `decdn_redemption_reconcile_failures_total`.
879 pub redemption_reconcile_failures: Counter,
880 /// Buyer-side reclaim-sweep attempts (`try_reclaim`) that failed — a failed
881 /// `getChannel`/`reclaimExpired` RPC, a receipt wait, an on-chain revert, or
882 /// a failed store write when clearing the local record after a reclaim/drop
883 /// (#906). Each is otherwise only a single `warn!` per hourly sweep; a
884 /// sustained rate means an expired channel's refundable deposit is not being
885 /// recovered (check the gas wallet / RPC). Pairs with the `error!`
886 /// escalation once the same channel fails `RECLAIM_ESCALATION_THRESHOLD`
887 /// consecutive sweeps. Operator-visible name:
888 /// `decdn_buyer_reclaim_failures_total`.
889 pub buyer_reclaim_failures: Counter,
890 /// Buyer-pool rows omitted from a successful store hydration because
891 /// their persisted values could not be decoded. Counted once per skipped
892 /// row per load attempt, so a persistent malformed row keeps the alert
893 /// active while healthy rows continue through buyer maintenance. Operator-
894 /// visible name:
895 /// `decdn_buyer_pool_store_skipped_undecodable_records_total`.
896 pub buyer_pool_store_skipped_undecodable_records: Counter,
897 /// Background low-water top-ups (#1146) that landed: a reused buyer channel
898 /// whose remaining deposit had fallen below its 20% low-water mark was
899 /// re-funded to the working deposit, so sustained miss pulls to that provider
900 /// keep flowing instead of silently stranding on a spent-down channel. The
901 /// healthy signal of the auto-refill path. Operator-visible name:
902 /// `decdn_buyer_topup_ok_total`.
903 pub buyer_topup_ok: Counter,
904 /// Background low-water top-ups (#1146) that did NOT cleanly land: the standing
905 /// allowance re-approval failed, the `topUp` submission/receipt errored or
906 /// reverted, OR the `topUp` landed on-chain but the local channel row vanished
907 /// or rotated during the RPC, or the credit itself faulted (escrowed-but-untracked
908 /// — `fund_pool` logs the tx at error! for reconcile). Folding the untracked case in here — rather than
909 /// counting it as `buyer_topup_ok` — means an operator alerting on this metric
910 /// sees stranded deposits. The refill is best-effort (the channel is simply left
911 /// un-topped), but a sustained rate means reused channels
912 /// are not being refilled and pull-through to busy providers will degrade as
913 /// their deposits drain (check the gas wallet / RPC / USDC balance). Operator-
914 /// visible name: `decdn_buyer_topup_failure_total`.
915 pub buyer_topup_failure: Counter,
916 /// Lane-store reconciliation the settlement watcher could not apply from a
917 /// poll tick: a `forget_lane` store write that failed while dropping a
918 /// reclaimed pool's lanes on a `PoolReclaimed` event. The failure is
919 /// swallowed (logged, and the cursor advances past it), so a non-zero rate
920 /// flags a struggling lane store or RPC rather than a stuck settlement.
921 /// Operator-visible name: `decdn_watcher_persist_failures_total`.
922 pub watcher_persist_failures: Counter,
923 /// Lane-store background flushes that failed (the dirty set is retained and
924 /// retried next tick). A sustained non-zero rate means the store cannot be
925 /// fsynced — the frontier-only replay surface is widening. Operator-visible
926 /// name: `decdn_lane_flush_failures_total`.
927 pub lane_flush_failures: Counter,
928 /// Slashes detected against this node's operator by the slash watcher
929 /// (`SlashJudge.Slashed`), counting each distinct `slashId` once across the
930 /// bring-up backfill and the live stream (#1032). A non-zero value means the
931 /// operator was slashed and should consider `decdn appeal slash` within the
932 /// 30-day window. Operator-visible name: `decdn_slashes_detected_total`.
933 pub slashes_detected: Counter,
934 /// `decdn_slash_resync_failures_total`: times the slash watcher's periodic
935 /// re-enumeration could not read chain state and kept the detected-slash
936 /// set it already had. The resync reports success upward so the event tail
937 /// keeps running, so a persistently failing repair moves nothing else.
938 /// Pairs with the `warn!` in `slash_watcher`'s `SlashSink::on_tick_complete`.
939 /// Field has no `_total` suffix because the `OpenMetrics` encoder appends it.
940 pub slash_resync_failures: Counter,
941 /// `decdn_slash_watcher_restarts_total` (#1032): distinct drift windows the
942 /// slash-detection watcher has entered, bumped once on the edge into the
943 /// error/backoff state. Pairs with `slash_watcher_down_seconds` to tell one
944 /// long outage from repeated flapping. Mirrors `staker_set_watcher_restarts`.
945 pub slash_watcher_restarts: Counter,
946 /// `decdn_slash_watcher_down_seconds` (#1032): seconds the slash-detection
947 /// watcher has been stuck in its per-tick backoff loop (`0` on a healthy
948 /// cycle), recomputed at scrape from `slash_watcher_down_since`. Mirrors the
949 /// other chain watchers — since `admin_v1_slashes` is always wired, this is
950 /// the only signal that a wedged watcher could be silently missing slashes.
951 pub slash_watcher_down_seconds: Gauge,
952 /// `decdn_staker_set_watcher_restarts_total` (#783): distinct drift
953 /// windows the [`crate::dht::chain_staker_set`] watcher has entered —
954 /// bumped once on the *transition* from a healthy cycle into the
955 /// error/backoff state, NOT on every backoff iteration of one continuous
956 /// outage. Each increment therefore brackets exactly one window in which
957 /// the cached active-staker set can drift from chain state — the module's
958 /// documented mid-run degradation. Because the stake-lane probe
959 /// reservation (#757) and the DHT `Store` admission path both read that
960 /// cached set, sustained restarts are revenue-impacting, not just a
961 /// discovery-health blip. The loop-level `tracing::warn!` in
962 /// `multiplexed_poller::run` (`"watcher RPC error; restarting after backoff"`)
963 /// is emitted in the same failed-tick `Err` arm that fires the `on_backoff`
964 /// hook this counter hangs off, and carries the underlying error; this
965 /// counter is the alertable rate. An idle poll tick (head has not advanced /
966 /// no new logs) is NOT an error and does not bump this. Field has no
967 /// `_total` suffix because the `OpenMetrics` encoder appends it.
968 pub staker_set_watcher_restarts: Counter,
969 /// `decdn_staker_set_watcher_resolve_failures_total` (#788): times an
970 /// operator-indexed event (`Reinstated` / `UnbondingRequested`) was
971 /// dropped because the follow-up `nodeIdOf(operator)` RPC failed. A dropped
972 /// resolution leaves the cached active set out of sync with chain state for
973 /// that operator until a later event, or until the watcher's cadence-gated
974 /// `getRegisteredNodes` re-enumeration corrects it — exactly the silent
975 /// drift these #783/#788 metrics exist to surface. That resync shortens the
976 /// window rather than bounding it: it is skipped while the route is errored,
977 /// and a failed read defers another interval.
978 /// Unlike a stream-level error, this does NOT trip a backoff/restart, so it
979 /// would otherwise move no metric at all. Pairs with the per-failure
980 /// `warn!` in [`crate::dht::capacity_bond_registry`]'s
981 /// `RegistrySink::on_operator_change`. Field has no `_total` suffix because
982 /// the `OpenMetrics` encoder appends it.
983 pub staker_set_watcher_resolve_failures: Counter,
984
985 /// `decdn_capacity_bond_registry_resync_failures_total`: times the
986 /// capacity-bond registry's periodic re-enumeration of `getRegisteredNodes`
987 /// could not read chain state and kept the projections it already had.
988 ///
989 /// That re-enumeration is the only systematic repair for an active-staker
990 /// set that has drifted from chain state, and it deliberately reports
991 /// success upward — returning an error would mark the watcher route errored
992 /// and stall event pickup, trading a stale set for no updates at all. So a
993 /// persistently failing resync moves nothing else. Two behaviours compound
994 /// it, both correlated with the RPC fault that caused the drift: the resync
995 /// is skipped entirely while the route is errored, and a failed read stamps
996 /// the cadence clock anyway, deferring the next attempt a further interval.
997 /// Pairs with the liveness gauge below, which is what catches the skipped
998 /// case. Field has no `_total` suffix because the `OpenMetrics` encoder
999 /// appends it.
1000 pub capacity_bond_registry_resync_failures: Counter,
1001 /// `decdn_staker_set_watcher_down_seconds` (#783, semantics corrected
1002 /// #788): true downtime — seconds the staker-set watcher has been in the
1003 /// error/backoff state, i.e. failing its `eth_getLogs` poll tick. Reads `0`
1004 /// for the entire life of any established cycle, however long or quiet (a
1005 /// healthy poll loop persists indefinitely, so this must NOT measure cycle
1006 /// age), and climbs only while the loop is between a failed cycle and the
1007 /// next successful re-establishment. Reads `0` until the first cycle is
1008 /// established after bootstrap. Recomputed at scrape time from a monotonic
1009 /// `down_since` timestamp. A poisoned lock reports `i64::MAX` (not `0`) so
1010 /// it trips the alert rather than masking an in-progress outage — the
1011 /// conservative direction for a downtime gauge. This is the drift-window
1012 /// depth gauge that `decdn_rpc_healthy` (a different watchdog) does not
1013 /// cover.
1014 pub staker_set_watcher_down_seconds: Gauge,
1015 /// `decdn_staker_set_active_count` (#783): current cached active-staker
1016 /// set size, sampled on every membership change the watcher applies. Pairs
1017 /// with `decdn_staker_set_watcher_down_seconds` to spot a collapse —
1018 /// e.g. the count holding flat while down-seconds climbs means the cache
1019 /// is frozen, not that the network genuinely lost operators.
1020 pub staker_set_active_count: Gauge,
1021 /// `decdn_staker_set_active_by_region{node_region}`: active-staker set
1022 /// size per operator-declared region (ADR 030), the source of the
1023 /// reference network map. Each node's registry holds the whole network, so
1024 /// one scrape maps every active node; aggregate across nodes with `max`,
1025 /// not `sum`. A region that drops to zero loses its series rather than
1026 /// exporting `0`, and the name is absent while no active node has a valid
1027 /// region. Label values pass [`Region::parse`], so the ISO
1028 /// allowlist bounds cardinality and an unparsable on-chain `regionHint`
1029 /// never reaches a label. Like `streams_active{direction}`, this labels a
1030 /// dimension, not a reason split, so the sibling-counter convention does
1031 /// not apply.
1032 staker_set_active_by_region: Family<NodeRegionLabels, Gauge>,
1033 /// `decdn_staker_set_active_unknown_region`: active stakers whose
1034 /// `regionHint` is empty or not an accepted region code. With the
1035 /// per-region family it sums to `decdn_staker_set_active_count` after each
1036 /// completed watcher tick. The count moves on each event and the region
1037 /// split at the end of the tick, so the two can differ between ticks and
1038 /// while the watcher is in backoff.
1039 pub staker_set_active_unknown_region: Gauge,
1040 /// `decdn_node_address_directory_size` (#831): current count of cached
1041 /// `NodeId → operator address` bindings the node-to-node pull path resolves
1042 /// against ([`crate::dht::node_address::ChainNodeAddressDirectory`]).
1043 /// Sampled on every `NodeRegistered` / `NodeDeregistered` the watcher
1044 /// applies. A binding the watcher misses (drift window) means the
1045 /// orchestrator cannot pay that provider and skips it, so a collapsed or
1046 /// frozen count here directly caps reachable upstream providers.
1047 pub node_address_directory_size: Gauge,
1048 /// `decdn_node_pull_attempts_total` (#831): node-to-node cache-miss pull
1049 /// orchestrations that found ≥1 candidate provider to try. Denominator for
1050 /// the success/corruption/unreachable rates below.
1051 pub node_pull_attempts: Counter,
1052 /// `decdn_node_pull_first_byte_seconds`: time to first byte of one paid
1053 /// node-to-node pull leg, from the start of its open (a dial included, when
1054 /// the node holds no warm connection to the peer) to the first bao bytes
1055 /// read off the upstream stream. One observation per leg. An adopted header
1056 /// handshake keeps the start of the handshake open, so its window includes
1057 /// the handshake. A leg that reads no bytes records nothing.
1058 #[default(Histogram::new(FIRST_BYTE_BUCKETS.to_vec()))]
1059 pub node_pull_first_byte_seconds: Histogram,
1060 /// `decdn_node_pull_success_total` (#831): pulls that delivered verified
1061 /// bytes from an upstream node.
1062 pub node_pull_success: Counter,
1063 /// `decdn_node_pull_no_providers_total` (#831): misses where discovery
1064 /// (DHT plus the origin-directory fallback) surfaced no provider — the blob
1065 /// is unavailable on the network, not a pull failure.
1066 pub node_pull_no_providers: Counter,
1067 /// `decdn_probe_cache_hits_total` (#1165): cache-miss pulls whose candidate
1068 /// walk STARTED from a live ADR 001 §Probe cache entry with at least one
1069 /// still-selectable provider. The DHT lookup and probe fanout are skipped —
1070 /// unless every cached provider fails, in which case the same fetch either
1071 /// falls through to a fresh lookup + probe (if attempt budget remains) or
1072 /// returns a clean miss (if the cached providers exhausted the budget first,
1073 /// the common case since an entry holds up to 10 providers but the budget is
1074 /// 3) — either way it stays counted here (the hit is "the cache had
1075 /// something worth trying", not "the cache delivered"). Field has no
1076 /// `_total` suffix because the
1077 /// `OpenMetrics` encoder appends it. With `probe_cache_misses` this is the
1078 /// hit ratio the TTL exists to buy; a ratio near zero means the TTL is
1079 /// shorter than the request inter-arrival time for hot blobs and the cache
1080 /// is pure overhead.
1081 pub probe_cache_hits: Counter,
1082 /// `decdn_probe_cache_misses_total` (#1165): cache-miss pulls that had to run
1083 /// a fresh DHT lookup + probe. Counts an entry that was absent, expired, OR
1084 /// fully suppressed (every cached provider negative-cached, wedged, no longer
1085 /// an active staker, or otherwise unselectable), and on the ranged pull leg an
1086 /// entry whose providers do not cover every block of the blob (#2195) — all
1087 /// cost the same network work, which is what this measures.
1088 pub probe_cache_misses: Counter,
1089 /// `decdn_probe_collection_latency_seconds` (ADR 001 §Probe response
1090 /// collection): the time from the start of the concurrent probes, dials
1091 /// included, to the end of collection — the early stop once enough holders
1092 /// answer, or the drain of the probe set, which `PROBE_TIMEOUT` bounds per
1093 /// probe. A round that sends no probe records nothing. Each probe round
1094 /// records one sample, so a ranged miss whose holders do not span the blob,
1095 /// and that then probes origin-directory candidates, records two (#2195).
1096 #[default(Histogram::new(PROBE_COLLECTION_BUCKETS.to_vec()))]
1097 pub probe_collection_latency_seconds: Histogram,
1098 /// `decdn_dht_lookup_round_ceiling_total` (#1145 review): a `find_providers` lookup was
1099 /// TRUNCATED at `MAX_LOOKUP_ROUNDS` while still finding closer nodes.
1100 ///
1101 /// Not an error — the providers already found are usable and the pull proceeds with them
1102 /// — but not nothing either, and a bare `debug!` would be invisible at the
1103 /// project's default `RUST_LOG=info`, so it is a counter. The ceiling exists because
1104 /// discovery's worst case
1105 /// has to be FINITE for `PULL_THROUGH_OUTER_SLACK` to budget for it at all; the cost is
1106 /// that a lookup which needs more rounds silently returns a smaller candidate set.
1107 ///
1108 /// So a sustained rate means this node's round ceiling is too low for its network size:
1109 /// every lookup is cut short, and `MAX_PROVIDER_ATTEMPTS` is choosing from a worse set of
1110 /// providers than the DHT could have offered. Kademlia converges in `O(log n)` rounds, so
1111 /// this is the metric that says "your network outgrew the constant".
1112 pub dht_lookup_round_ceiling: Counter,
1113 /// `decdn_node_pull_corruption_total` (#831): an upstream served
1114 /// hash-mismatched bytes for a paid pull (Byzantine / buggy provider). A
1115 /// sustained nonzero rate means a peer is taking payment for wrong content;
1116 /// the cache engine rejects the bytes, but the USDC was still spent.
1117 pub node_pull_corruption: Counter,
1118 /// `decdn_node_pull_unreachable_total` (#831): a probe or pull to a
1119 /// candidate failed at the transport (scored [`Outcome::Unreachable`]).
1120 ///
1121 /// [`Outcome::Unreachable`]: decdn_reputation::Outcome::Unreachable
1122 pub node_pull_unreachable: Counter,
1123 /// `decdn_node_region_latency_penalty_total` (#1177): a probed peer
1124 /// self-attested this node's own region yet answered slower than the ADR 030
1125 /// latency ceiling, so it was penalized in local reputation
1126 /// ([`Outcome::RegionLatencyMismatch`]). A sustained rate points at
1127 /// region-spoofing peers (or a genuinely mis-set local `identity.region`).
1128 ///
1129 /// [`Outcome::RegionLatencyMismatch`]: decdn_reputation::Outcome::RegionLatencyMismatch
1130 pub node_region_latency_penalty: Counter,
1131 /// `decdn_node_pull_pool_open_failures_total` (#831): a buyer
1132 /// `open_or_reuse_pool` failed before a pull could start. This is the
1133 /// node's own payment-side fault (gas, RPC, expired pool), NOT the
1134 /// provider's — a sustained rate means node→node buying is wedged. This is
1135 /// the *unlabeled total* across all causes; the
1136 /// `pool_open_failures_*_total` family below (#966) breaks the
1137 /// `openPool`-tx failures out by cause so an operator can tell a
1138 /// misconfiguration (`insufficient_deposit`) from infrastructure
1139 /// (`rpc_error`). It also covers store/expired-reclaim causes the by-reason
1140 /// family does not, so the two are not expected to sum equal.
1141 ///
1142 /// **One failure moves this counter once.** A site increments it if and only
1143 /// if it marks the error `OpenReported`, which is what stops the classifier
1144 /// in `node_origin` from restating a failure the open task already counted.
1145 /// Against `node_pull_attempts_total` this reads above 1.0 legitimately: a
1146 /// pull orchestration meters one attempt and may open a lane per candidate
1147 /// and per assembled run. A clean 2× ratio is the signature of a leg that
1148 /// meters without marking (#2072).
1149 pub node_pull_pool_open_failures: Counter,
1150 /// `decdn_pool_open_failures_insufficient_deposit_total` (#966): a buyer
1151 /// `openPool` tx reverted because the node's USDC balance/allowance could
1152 /// not cover the deposit, or the deposit was zero — either as requested, or
1153 /// as the balance delta actually received under a fee-on-transfer token.
1154 /// Both zero cases revert the same argument-less `ZeroAmount`, so this
1155 /// counter cannot separate them; the wallet balance is what distinguishes a
1156 /// misconfigured deposit from a token that shaved it. A token that reverts
1157 /// in the older `Error(string)` style lands here too, matched on its message
1158 /// rather than a custom-error selector. A *misconfiguration*
1159 /// signal either way — the fix is operator-side (fund the wallet, raise the
1160 /// configured deposit), not infrastructure. A plain counter
1161 /// field carries no label dimension (a labeled series would need a `Family`),
1162 /// so the issue's `{reason=…}` split is realized as
1163 /// three sibling counters (mirroring `dht_rate_limit_rejected_*`); the
1164 /// `reason` value is the field-name token. The `OpenMetrics` encoder appends
1165 /// the `_total` suffix.
1166 pub pool_open_failures_insufficient_deposit: Counter,
1167 /// `decdn_pool_open_failures_contract_revert_total` (#966): a buyer
1168 /// `openPool` tx reverted on-chain for a reason other than insufficient
1169 /// deposit (provider not active, a paused contract, a mined revert whose
1170 /// reason is not recoverable from the receipt). The deposit was not
1171 /// escrowed; the cause is on-chain state, not this node's wallet or RPC.
1172 pub pool_open_failures_contract_revert: Counter,
1173 /// `decdn_pool_open_failures_rpc_error_total` (#966): a buyer
1174 /// `openPool` submit or receipt wait failed at the transport layer (no
1175 /// revert data) — connectivity, a timed-out receipt, a nonce blip. A
1176 /// *transient infrastructure* signal; retrying typically clears it. Pair
1177 /// with the two reverting counters above to tell "operator under-funded the
1178 /// wallet" from "the RPC endpoint is flaky".
1179 pub pool_open_failures_rpc_error: Counter,
1180 /// `decdn_node_pull_too_large_total` (#840): the bytes received from a
1181 /// selected upstream crossed this node's `max_blob_size` ceiling, so the buyer
1182 /// aborted the pull (#1895). The upstream's `total_bytes` claim plays no part.
1183 /// Like a channel-open failure this is a buyer-side policy decision, NOT
1184 /// necessarily provider misbehavior (the provider may legitimately serve
1185 /// larger blobs to nodes with a higher ceiling), so it does not tar the
1186 /// provider's reputation. A sustained rate
1187 /// means this node's ceiling is below the content it is trying to warm.
1188 pub node_pull_too_large: Counter,
1189 /// `decdn_node_pull_rate_above_ceiling_total` (#1375): a selected upstream
1190 /// signed an open-stage `StreamResponse` quoting a per-MB rate above this
1191 /// node's effective buyer ceiling (the lower of the candidate's probe rate and
1192 /// the configured `cache.max_rate_per_mb`), so the buyer refused before paying
1193 /// any voucher. Sibling of [`Self::node_pull_too_large`]: a buyer-side policy
1194 /// decision that does NOT tar the provider (an over-*config* quote is our tight
1195 /// policy; an over-*probe* quote is a possible bait-and-switch we do not
1196 /// adjudicate here). A sustained rate is the operator's signal that nodes are
1197 /// being probed for rate manipulation — the one refusal on this path that
1198 /// guards a slashable offense, so it earns a counter of its own.
1199 pub node_pull_rate_above_ceiling: Counter,
1200 /// `decdn_serve_economics_refused_total` (ADR 041): candidates existed for a
1201 /// cache-miss buy but every one quoted above this node's serve-economics buy
1202 /// ceiling, so the buyer declined an unprofitable relay rather than paying for
1203 /// bytes it cannot resell for a margin. Sibling of
1204 /// [`Self::node_pull_rate_above_ceiling`]: a buyer-side policy decision, so it
1205 /// does not tar the provider's reputation. The node still signs the client a
1206 /// `NotFound` for this — the pricing floor never reaches the wire — so a
1207 /// sustained rate is visible only here, and means this node's serve economics
1208 /// (or the market it is buying into) are too tight to relay profitably.
1209 ///
1210 /// The aggregate over both regime siblings below, mirroring the
1211 /// `pool_open_failures_*` shape: bumped on every refusal beside the regime
1212 /// counter, so a dashboard can rate the total and drill into the split.
1213 pub serve_economics_refused: Counter,
1214 /// `decdn_serve_economics_refused_warming_total` (ADR 041): the subset of
1215 /// [`Self::serve_economics_refused`] where the source still had warming
1216 /// allowance, so the ceiling was the market price `max(sell, amortized)` and
1217 /// the market itself is above it. Remedy: the market is too hot to relay
1218 /// into — there is no local config fix. See [`ServeEconomicsRegime`].
1219 pub serve_economics_refused_warming: Counter,
1220 /// `decdn_serve_economics_refused_amortized_total` (ADR 041): the subset of
1221 /// [`Self::serve_economics_refused`] where the source's warming allowance was
1222 /// spent (or warming is off), so the ceiling was already the amortized floor.
1223 /// Remedy: this node's own sell rate or margin config is too tight. A rising
1224 /// split toward this regime is the operator-actionable one. See
1225 /// [`ServeEconomicsRegime`].
1226 pub serve_economics_refused_amortized: Counter,
1227 /// `decdn_node_pull_timeout_total` (#857): a buyer→upstream pull hit one of this node's
1228 /// own deadlines. Like a channel-open failure this is a buyer-side condition (a possibly
1229 /// mis-sized local budget), NOT evidence the provider is unreachable, so it does NOT tar
1230 /// the provider's local reputation. Distinct from `node_pull_through_timeouts` (the
1231 /// delivery handler's own serving deadline).
1232 ///
1233 /// It fires on **two** budgets, and they have different remedies (#1145 review):
1234 ///
1235 /// - the STREAM-OPEN stage exceeding `node_pull_timeout_sec`; and
1236 /// - a pull that has received no first byte within `node_pull_stall_window_sec`. Before
1237 /// the first chunk, the throughput floor is measuring the server's time-to-FIRST-byte,
1238 /// which scales with blob size (the serve path materialises the whole bao encoding
1239 /// before it can emit chunk #1) — so it is our deadline, not the peer's fault, and it
1240 /// lands here rather than on `node_pull_stalled_total`.
1241 ///
1242 /// A sustained rate therefore means one of those two is too tight for the upstreams this
1243 /// node selects — and for the large-blob case it is `node_pull_stall_window_sec`, not
1244 /// `node_pull_timeout_sec`, that wants raising.
1245 ///
1246 /// The peer is not scored, but it IS suppressed briefly: see `REFUSAL_SUPPRESSION_TTL`.
1247 /// Exonerating a peer and ignoring it are different things, and a peer that accepts a
1248 /// stream and then says nothing lands here.
1249 pub node_pull_timeout: Counter,
1250 /// `decdn_node_pull_voucher_rejected_total` (#857): an upstream rejected a
1251 /// voucher this node presented mid-pull (an amount or bytes regression, a
1252 /// spent capability cap, or a voucher addressed to the wrong pool or
1253 /// provider). This is the node's own payment-side fault, NOT the provider's,
1254 /// so it does NOT tar the provider's reputation. A sustained rate means this
1255 /// node's buyer lanes are drifting out of sync with what upstreams accept.
1256 pub node_pull_voucher_rejected: Counter,
1257 /// `decdn_node_pull_pool_wedged_total` (#1145 review): an upstream rejected our
1258 /// voucher on terms this lane cannot recover from, while the pool's deposit is STILL
1259 /// ESCROWED — an amount or bytes regression, a spent capability cap, an expired
1260 /// capability, or a voucher addressed to the wrong pool or provider.
1261 ///
1262 /// The pool row is KEPT (it is the only thing that can still reclaim the deposit) and
1263 /// the PROVIDER is suppressed for a fixed one-hour window. The deposit is not
1264 /// stranded: it is shared across every lane, so it stays available to every other
1265 /// provider throughout, and the pool has no expiry of its own to wait on.
1266 ///
1267 /// **A sustained rate is a provider this node keeps failing to pay.** A spike means
1268 /// buyer watermarks are drifting out of sync with what upstreams have committed — the
1269 /// desync tracked in #1122 — and the deposit sizing and pool count should be reviewed
1270 /// alongside it. One tick costs an hour of that provider's capacity, not the deposit.
1271 pub node_pull_pool_wedged: Counter,
1272 /// `decdn_node_pull_refused_total` (#1144): a selected upstream refused
1273 /// delivery up front (a `StreamResponse` with `ok == false`). Counts every
1274 /// wire class, and none of them tars the provider's reputation. It is not
1275 /// split by class (a labeled series would need a `Family`). Most refusals
1276 /// are honest and benign: a `NotFound` is simply a healthy-but-empty node, so
1277 /// a sustained rate here usually means content discovery is steering this node
1278 /// at upstreams that do not hold the blob — not that the upstreams are bad.
1279 pub node_pull_refused: Counter,
1280 /// `decdn_node_pull_refused_unattributable_total` (#1520): the subset of
1281 /// [`Self::node_pull_refused`] whose wire code this node cannot pin on the
1282 /// upstream — `NotFound` and `Unfunded`, i.e. `RefusalVerdict::Transient`.
1283 /// Those briefly suppress the `(peer, hash)` pair without touching reputation,
1284 /// except an open-stage `NotFound` on a node-origin leg, which is backpressure
1285 /// and suppresses nothing unless it outlasts the wait budget (#2178).
1286 ///
1287 /// Split out because it is the shape a *buyer-side* misconfiguration takes:
1288 /// `Unfunded` is what a seller signs when it refuses OUR pool for its
1289 /// deposit, and `NotFound` when our signer sits at its floor cap, so a node
1290 /// whose own deposit is too small to buy anything sees 100% of its pulls
1291 /// refused here.
1292 ///
1293 /// Read it with one caveat: `NotFound` also carries the PEER's load shed,
1294 /// which the code respects rather than punishes. So a network-wide load event
1295 /// drives this ratio to ~1 for a reason no local change fixes. A rate
1296 /// approaching `node_pull_refused` therefore means "nobody is serving me",
1297 /// which is *usually* local (deposit, binding) but is worth confirming against
1298 /// peer health first; a small fraction is the healthy "that peer did not
1299 /// have it".
1300 pub node_pull_refused_unattributable: Counter,
1301 /// `decdn_node_upstream_rate_limited_total` (#1986): a probe or pull to a
1302 /// candidate was shed at the transport with `APP_ERR_RATE_LIMITED` (`0x10`,
1303 /// ADR 013 §Application Error Codes) — the upstream's connection limiter,
1304 /// probe limiter, or per-connection stream cap refused the work before any
1305 /// signed message existed. The transport-level twin of a load-shed `NotFound`
1306 /// refusal, treated the same way: NO reputation outcome is recorded, and the
1307 /// `(peer, hash)` pair is suppressed for the short refusal TTL except on a
1308 /// node-origin leg, where the shed is backpressure (#2178). So this is the
1309 /// only trace the event leaves at the default `RUST_LOG=info`.
1310 ///
1311 /// A sustained rate is peer-side load, not a local fault — but a rate that
1312 /// tracks `node_pull_unreachable` is worth a look: a `global-full` shed skips
1313 /// the upstream's close ack-wait and can still arrive as a bare drop, which
1314 /// lands in the unreachable counter instead of here.
1315 pub node_upstream_rate_limited: Counter,
1316 /// `decdn_node_pull_backpressure_backoffs_total` (#2178): a ranged run's
1317 /// upstream refused the stream open for backpressure (`NotFound` or a
1318 /// transport rate-limit), no other candidate covered the
1319 /// still-missing gap, and the assembly waited to ask the same upstream again
1320 /// instead of dropping it. The common cause is this node's own other pulls
1321 /// filling the upstream's per-signer live cap: the refusal clears as those
1322 /// pulls pay.
1323 ///
1324 /// Each tick is one completed wait, and each wait follows one refusal already
1325 /// counted in `node_pull_refused_unattributable` or
1326 /// `node_upstream_rate_limited`. A sustained rate most often means an
1327 /// upstream's cap is too small for the concurrency this node's clients drive
1328 /// through it.
1329 pub node_pull_backpressure_backoffs: Counter,
1330 /// `decdn_node_pull_backpressure_exhausted_total` (#2178): the only holder of
1331 /// a still-missing range kept refusing for backpressure through the whole
1332 /// wait budget, so the assembly ended short and every serve stream attached
1333 /// to it was truncated. The holder is then suppressed for the hash for the
1334 /// short refusal TTL, and a `warn!` names it.
1335 ///
1336 /// Zero is the expected value. A non-zero rate means a refusal that waiting
1337 /// does not clear: a holder whose cap never releases, or one of the refusals
1338 /// the seller collapses onto `NotFound` that is not a cap at all.
1339 pub node_pull_backpressure_exhausted: Counter,
1340 /// `decdn_node_pull_leg_no_progress_total` (#2194): a pull leg streamed and
1341 /// finished cleanly, yet moved neither the gap's paid frontier nor the store's
1342 /// delivered frontier. The drive ends the gap rather than re-open and re-pay the
1343 /// same range. An upstream leg is not scored: its `(peer, hash)` pair is
1344 /// suppressed briefly and the run re-plans onto another source. A `warn!` names
1345 /// the range, both frontiers and the channel's paid wire since the leg opened.
1346 ///
1347 /// Zero is the expected value. A tick means this node's store did not keep a
1348 /// leg's bytes, its ledger did not record the leg's payment, or the upstream
1349 /// ended the stream without taking the leg's final proof.
1350 pub node_pull_leg_no_progress: Counter,
1351 /// `decdn_node_pull_stalled_total` (#1797): an upstream's throughput fell below the
1352 /// floor mid-stream — the bytes across `node_pull_stall_window_sec` dropped under
1353 /// `node_pull_min_throughput_bps` — so the pull was abandoned after at least one byte had
1354 /// arrived. Split from `node_pull_timeout` (the same abort before the first byte) only as
1355 /// a metric: BOTH are requester-local policy and neither tars the provider's reputation,
1356 /// because a sub-floor stream may be slow for reasons the peer cannot be blamed for and a
1357 /// throughput signal is spoofable. A sustained rate points at flaky upstreams or a
1358 /// `node_pull_min_throughput_bps` too high (or `node_pull_stall_window_sec` too tight)
1359 /// for the network.
1360 pub node_pull_stalled: Counter,
1361 /// `decdn_node_pull_local_fault_total` (#1145 review): a pull failed for a reason
1362 /// that is OURS — a broken signer, an encode fault, a bad range computation, an
1363 /// unusable deadline config, a buyer channel open this node's own state defeated
1364 /// (an unreadable or unwritable channel store, a poisoned open lock, a panicked open
1365 /// task, a wallet that cannot fund a deposit; #1560), or a cache store that cannot
1366 /// take the pulled bytes (a full or read-only disk, a cache code bug; #2286) — and
1367 /// the upstream was exonerated.
1368 ///
1369 /// The only counter here that says nothing about the network. Any sustained rate is
1370 /// an emergency: a node that cannot sign a voucher or store a byte cannot complete a
1371 /// pull, so every pull it attempts will fail. These failures land here, not on the
1372 /// honest providers the node tries: scored against them, they would show as a node
1373 /// steadily blaming a healthy network in its own reputation scores.
1374 ///
1375 /// Counted PER CANDIDATE, not per request: one node-wide fault moves this by up to
1376 /// `MAX_PROVIDER_ATTEMPTS` for a single client request, because the walk tries each
1377 /// candidate and fails identically on all of them. Read the rate, not the absolute,
1378 /// and do not infer the number of affected requests from it.
1379 pub node_pull_local_fault: Counter,
1380 /// `decdn_node_pull_pool_open_pending_total` (#1143): a buyer channel open
1381 /// was still in flight when the per-candidate budget expired, so the pull moved
1382 /// to the next candidate while the open continued in the background.
1383 ///
1384 /// NOT a failure — kept separate from `node_pull_pool_open_failures` because
1385 /// the diagnosis is different: a failure says the tx reverted or the wallet is
1386 /// under-funded, whereas this says the open is simply *not done yet*. It scores no
1387 /// reputation: a wedged open is our lane, not evidence about the peer.
1388 ///
1389 /// # Two distinct causes, one counter
1390 ///
1391 /// 1. the node's own chain lane (a slow L2, a stuck nonce) is slower than
1392 /// `CHANNEL_OPEN_CALLER_BUDGET` — the interesting one; and
1393 /// 2. a boot or idle **reconcile** holds the provider's open slot.
1394 ///
1395 /// The verdict is the same for both — try the next candidate, score nothing — which
1396 /// is why they share a counter. But the *diagnosis* is not: reconcile runs at every
1397 /// boot, so a restart produces a burst here that means nothing is wrong. Read a
1398 /// sustained rate as a chain-lane signal only once it outlives a restart.
1399 pub node_pull_pool_open_pending: Counter,
1400 /// `decdn_node_pull_progress_persist_failures_total` (#852): a pull paid ≥1
1401 /// voucher but persisting the buyer channel's resume watermark
1402 /// (`record_progress`) failed. The bytes were delivered, but the channel's
1403 /// stored `nonce`/`bytes`/`amount` now lags what the upstream accepted — the
1404 /// next reuse of this channel will re-sign a stale voucher and be rejected. A
1405 /// non-zero count means a provider is at risk of becoming unusable until
1406 /// channel rotation.
1407 pub node_pull_progress_persist_failures: Counter,
1408 /// `decdn_node_pull_progress_dropped_total` (#1145 review): a pull paid ≥1
1409 /// voucher, but by the time the watermark was written the provider's slot had
1410 /// been replaced by a newer open — so the write was skipped rather than clobber
1411 /// the replacement, and that voucher's progress is gone.
1412 ///
1413 /// Sibling of `node_pull_progress_persist_failures_total`, which only counts the
1414 /// `Err` path; this is the `Ok`-but-dropped path, which is otherwise invisible.
1415 /// The rare benign case is a channel rotating mid-pull. A *sustained* rate means
1416 /// a `channel_id`-plumbing bug, and every tick is real USDC whose watermark was
1417 /// discarded — so this is the counter to alert on, not just to look at.
1418 pub node_pull_progress_dropped: Counter,
1419 /// `decdn_node_pull_progress_superseded_total` (#1145 review): a pull's watermark write was
1420 /// REGRESSED because a CONCURRENT pull on the same shared channel ledger had already
1421 /// persisted a higher (correct) watermark. Benign and EXPECTED under the shared
1422 /// `BuyerLedgers` — concurrent pulls on one channel are routine — because the monotonic
1423 /// store keeps the winner's higher value, so no voucher is lost. Split from
1424 /// `node_pull_progress_persist_failures_total` (a real store-write failure that leaves the
1425 /// watermark lagging) so ordinary settle races do not drown out a genuine persist fault.
1426 pub node_pull_progress_superseded: Counter,
1427 /// `decdn_node_pull_recovery_step_total` (#1530): a node→node miss fill ran a
1428 /// funding recovery step (ADR 003 § Funding recovery) that funds the next
1429 /// pass: every candidate refused this node's funding, so the step topped the
1430 /// buyer pool up, opened a new pool in place of one that accepts no more
1431 /// funds, or settled a pool that already holds its working deposit while the
1432 /// upstreams catch up.
1433 ///
1434 /// Expected to be RARE once `buyer_working_deposit_micro_usdc` is sized for the
1435 /// blobs this node pulls: the low-water refill should refill a
1436 /// pool long before a fill runs it dry. A sustained rate means the working
1437 /// deposit is too small for the blob sizes in play, and every tick is a
1438 /// transaction plus a settlement wait a client sat through.
1439 ///
1440 /// Distinct from `decdn_buyer_topup_ok_total`, which counts on-chain top-ups from
1441 /// BOTH legs: this one isolates the recovery step, so the low-water refill's
1442 /// routine traffic cannot hide it.
1443 pub node_pull_recovery_step: Counter,
1444 /// `decdn_node_pull_recovery_step_refused_total` (#1530): a fill's funding
1445 /// recovery step had no way to fund the next pass: the working deposit is
1446 /// zero (funding off), the top-up landed nothing, or the funding
1447 /// transaction failed (allowance, revert, RPC, or a mined `topUp` the local
1448 /// pool row could not credit).
1449 ///
1450 /// A failed funding transaction also ticks `decdn_buyer_topup_failure_total`.
1451 pub node_pull_recovery_step_refused: Counter,
1452 /// `decdn_node_pull_through_timeouts_total` (#831): cache-miss pull-through
1453 /// attempts the delivery handler abandoned at its deadline. Distinguishes a
1454 /// slow/wedged upstream from a genuine miss (both otherwise return
1455 /// `NotFound`). Covers BOTH reactive fill tiers — node→node (#831) and the
1456 /// local-origin populate (#1116) — so it is not solely a node→node-health
1457 /// signal; a cache-only operator's own wedged origin bumps it too.
1458 pub node_pull_through_timeouts: Counter,
1459 /// `decdn_node_pull_through_errors_total` (#831): cache-engine errors (not
1460 /// clean misses) hit while filling a miss on the delivery path — a real
1461 /// store/pull fault, surfaced as `NotFound` to the client but logged + bumped
1462 /// here so it isn't silent. Covers BOTH reactive fill tiers: node→node (#831)
1463 /// and the local-origin populate (#1116).
1464 pub node_pull_through_errors: Counter,
1465 /// `decdn_node_pull_through_window_paused_total` (#856): times the
1466 /// window-paced serve loop paused the upstream pull because the per-request
1467 /// unrecouped frontier (`bytes pulled − bytes paid`) reached the ramped
1468 /// credit window (ADR 003 §Credit window, #1669) and it waited for the
1469 /// downstream voucher to clear. A high rate is benign (the window is
1470 /// doing its job pacing speculation); a flat zero under real pull-through
1471 /// traffic means the window never binds.
1472 pub node_pull_through_window_paused: Counter,
1473 /// `decdn_node_pull_through_min_draw_waits_total` (#2061): times the
1474 /// window-paced serve loop paused the upstream pull because the ramped
1475 /// window had room, but less than the minimum draw of half the window, and
1476 /// no serve leg was parked at the pull's frontier. Each draw opens a new
1477 /// upstream request, so the pause batches the room into fewer, larger draws.
1478 /// A high rate is benign; it grows with the number of vouchers per draw.
1479 pub node_pull_through_min_draw_waits: Counter,
1480 /// `decdn_node_pull_through_wait_seconds`: how long one window-paced pause
1481 /// (window full or minimum draw) held the upstream pull before a downstream
1482 /// payment or a parked serve leg released it, or its leg cancelled it. One
1483 /// observation per pause.
1484 /// Most pauses end within a voucher round trip; a tail near the top bucket
1485 /// means a payer that stalls or a pacing regression that parks the pull
1486 /// with no serve leg left to wake it. A pause past `PULL_WAIT_WARN_AFTER` (45 s)
1487 /// also logs a warning.
1488 #[default(Histogram::new(FIRST_BYTE_BUCKETS.to_vec()))]
1489 pub node_pull_through_wait_seconds: Histogram,
1490 /// `decdn_node_pull_through_client_abandoned_total` (#856): window-paced
1491 /// serves the requesting client dropped or underpaid mid-pull, so the node
1492 /// aborted the upstream pull and abandoned the partial fill. The per-request
1493 /// loss is bounded to the ramped credit window; a sustained rate flags a leech.
1494 /// A node-side fault on the serve leg is not an abandon: it counts on
1495 /// `decdn_serve_stream_node_fault_total` only. Nor is an owed chunk that passes
1496 /// its proof-wait ceiling while its connection still sends: a slow path or a
1497 /// busy sibling stream can cause that, and the stream ends on the declined or
1498 /// abandoned serve reason counter only. Every abandon here also ends the
1499 /// inbound stream on one serve reason counter: a spent per-chunk proof budget
1500 /// on `decdn_serve_stream_proof_budget_exhausted_total`, a rejected voucher on
1501 /// `decdn_serve_stream_voucher_rejected_total`, and a client that drops on
1502 /// `decdn_serve_stream_client_declined_total` before it pays or
1503 /// `decdn_serve_stream_client_abandoned_total` after.
1504 pub node_pull_through_client_abandoned: Counter,
1505 /// `decdn_node_pull_through_upstream_verify_failed_total` (#856/#915): a
1506 /// serve-miss pull ingested upstream bytes that failed bao verification against
1507 /// the content root (ADR 038) — a chunk group rejected mid-stream (the
1508 /// bait-and-switch case), a whole-blob root mismatch, or an over-delivery past
1509 /// the promised wire. The partial fill is never promoted, the
1510 /// upstream is scored `Corruption`, and the
1511 /// client's own bao decoder rejects the forwarded bytes. Distinct from
1512 /// `node_pull_corruption` (the buffered orchestration's own check). A sustained
1513 /// rate means clients are being served
1514 /// corrupt-upstream bytes through this node. Field has no `_total` suffix
1515 /// because the `OpenMetrics` encoder appends it.
1516 pub node_pull_through_upstream_verify_failed: Counter,
1517 /// `decdn_local_outboard_serves_total` (#1130): times the node served a
1518 /// whole-blob cache miss by streaming straight from its own configured
1519 /// origin (http/s3/fs) into the paying client while filling
1520 /// the local cache store beside it — the stream-while-store path, entered only when
1521 /// the origin publishes a `{H}.obao4` pre-order outboard alongside the
1522 /// object. Incremented once per entry into `serve_via_backend_origin`,
1523 /// before any admission guard, so it also counts requests this tier later
1524 /// rejects (deposit/leech/size) — it marks which SERVE TIER fired, not
1525 /// whether the fill ultimately succeeded. A miss that instead falls back to
1526 /// the buffered `populate_local` path (no outboard, or this tier declined)
1527 /// never bumps this counter, which is what lets a test or dashboard tell
1528 /// the two fill strategies apart deterministically instead of by timing.
1529 pub local_outboard_serves: Counter,
1530 /// `decdn_serve_cache_hit_total`: paid serves the blob-availability gate
1531 /// classified as servable from a COMPLETE locally-held blob, before any fill
1532 /// tier runs. One bump per request the gate classifies, in
1533 /// `handlers::client::dispatch`.
1534 ///
1535 /// Counted BEFORE the load-shed admission call, so a shed refusal still
1536 /// records the availability decision it refused. That keeps the hit rate a
1537 /// property of the store rather than of the node's current pressure — a
1538 /// node shedding hard would otherwise report a hit rate that collapses
1539 /// exactly when an operator most needs to read it.
1540 ///
1541 /// This is the serve-path twin of `decdn_cache_hits_total`, and the two are
1542 /// not interchangeable: the cache-crate counter meters
1543 /// `CacheEngine::get()`, the whole-blob buffered read, which the paid serve
1544 /// path never calls — it streams through `export_bao_range_stream` instead.
1545 /// A node that serves only paying clients therefore holds
1546 /// `decdn_cache_hits_total` at zero however full its store is. Its partner
1547 /// `decdn_cache_misses_total` is NOT confined to `get` — the fill tiers bump
1548 /// it too — so that pair reads as a populated, permanently-0% ratio rather
1549 /// than as an empty one, which is why the serve path needs its own family.
1550 ///
1551 /// Sibling of `serve_cache_partial_hit` and `serve_cache_miss`. At most one
1552 /// of the three is bumped per request, and two requests that reach
1553 /// `serve_audit` bump none: a withdrawn hash (operator evict or corruption
1554 /// quarantine) is refused for `evicted_since_probe`, and a `serve_audit` store
1555 /// fault is refused for `internal_error`. Neither is an availability class — one
1556 /// is a refusal, the other is the store failing to answer — so the ratio's
1557 /// denominator deliberately excludes both. Field has no `_total` suffix
1558 /// because the `OpenMetrics` encoder appends it.
1559 pub serve_cache_hit: Counter,
1560 /// `decdn_serve_cache_partial_hit_total`: paid serves admitted from a blob
1561 /// that is not `Complete` but whose held chunk groups already cover the
1562 /// requested span (#1506). Counted apart from `serve_cache_hit` because the
1563 /// two answer different questions — this one is the payoff of the
1564 /// range-keyed partial-holder advertisement a node makes over
1565 /// `cdn/probe/v1` and the DHT, and folding it into the plain hit would hide
1566 /// whether that mechanism carries any traffic. Both are hits for hit-rate
1567 /// purposes.
1568 pub serve_cache_partial_hit: Counter,
1569 /// `decdn_serve_cache_miss_total`: paid serves the gate could not satisfy
1570 /// from locally-held bytes. Counts the ADMISSION decision and nothing
1571 /// downstream of it — a fill tier may never run at all, because the bump
1572 /// precedes the load-shed gate, the pre-spend floor reservation, and the
1573 /// `pull_authorized` check that every tier is gated on. A shed refusal, a
1574 /// floor refusal and an unbound request therefore all count here having
1575 /// asked no origin and no peer. Where a tier does run and satisfies the
1576 /// blob, the request still counts here; the refusal siblings
1577 /// (`serve_stream_rejected_*`) say whether it ended in a refusal.
1578 ///
1579 /// Also absorbs one non-absence: `partial_hit_size` resolves the local
1580 /// bitfield with `.ok()?`, so a store fault while answering "do my held
1581 /// ranges cover this span?" reads as "they do not" and lands here. That is
1582 /// the conflation the `serve_audit` `Err` arm exists to avoid one branch
1583 /// earlier, and it bounds how clean a reading of this counter can be.
1584 pub serve_cache_miss: Counter,
1585 /// `decdn_serve_first_byte_hit_seconds`: time to first byte of a paid serve
1586 /// the availability gate classed as a hit (complete or partial), from the
1587 /// decoded request to the first `ChunkData` frame written. The first frame
1588 /// rides the opening credit window, so no client payment round trip is in
1589 /// the window; one `getPool` chain read is, when the pool view has no entry
1590 /// for the pool. A stream that ends before any frame records nothing.
1591 /// Sibling of `serve_first_byte_miss_seconds` on the cache-class axis.
1592 #[default(Histogram::new(FIRST_BYTE_BUCKETS.to_vec()))]
1593 pub serve_first_byte_hit_seconds: Histogram,
1594 /// `decdn_serve_first_byte_miss_seconds`: time to first byte of a paid serve
1595 /// the availability gate classed as a miss — the same window as
1596 /// `serve_first_byte_hit_seconds`, which here also covers discovery and the
1597 /// upstream or origin fill that produces the first frame. A window-paced
1598 /// fill streams its first frame early; a buffered fill completes the whole
1599 /// blob first, so on that tier the value grows with blob size.
1600 #[default(Histogram::new(FIRST_BYTE_BUCKETS.to_vec()))]
1601 pub serve_first_byte_miss_seconds: Histogram,
1602 /// `decdn_serve_admission_seconds`: the admission phase of a paid serve's
1603 /// time to first byte, from the decoded request to the load-shed admit. It
1604 /// covers the binding check, the deny gates, the `getPool` and
1605 /// `getAuthorization` chain reads on a view miss, and the availability
1606 /// audit. The class is unknown until the audit ends, so this phase has no
1607 /// hit/miss siblings. Records only for a stream that writes its first frame,
1608 /// so the three phase histograms and the total share one population.
1609 #[default(Histogram::new(FIRST_BYTE_BUCKETS.to_vec()))]
1610 pub serve_admission_seconds: Histogram,
1611 /// `decdn_serve_signer_auth_cached_total`: admit-path signer confirms the
1612 /// signer-auth cache answered, with no `getAuthorization`. A `Registered`
1613 /// read answers for good; an `Unregistered` one for a short window.
1614 pub serve_signer_auth_cached: Counter,
1615 /// `decdn_serve_signer_auth_first_read_total`: admit-path signer confirms for
1616 /// a `(pool, signer)` the node holds no read of: a new signer, or one
1617 /// evicted from the bounded cache. Each waits on a `getAuthorization`, its
1618 /// own or one already in flight for the pair.
1619 pub serve_signer_auth_first_read: Counter,
1620 /// `decdn_serve_signer_auth_reread_total`: admit-path signer confirms that
1621 /// re-read an `Unregistered` signer, because the read aged out or the
1622 /// projection has since folded a redemption by it. Each waits on a
1623 /// `getAuthorization`, its own or one already in flight for the pair.
1624 pub serve_signer_auth_reread: Counter,
1625 /// `decdn_serve_response_hit_seconds`: the response phase of a paid hit,
1626 /// from the load-shed admit to the signed `StreamResponse` written.
1627 #[default(Histogram::new(FIRST_BYTE_BUCKETS.to_vec()))]
1628 pub serve_response_hit_seconds: Histogram,
1629 /// `decdn_serve_response_miss_seconds`: the response phase of a paid miss,
1630 /// from the load-shed admit to the signed `StreamResponse` written. It
1631 /// covers the floor reservation, the origin size and range probes, and on a
1632 /// node-to-node fill the upstream discovery, pool open and handshake. A
1633 /// buffered fill imports the whole blob in this phase.
1634 #[default(Histogram::new(FIRST_BYTE_BUCKETS.to_vec()))]
1635 pub serve_response_miss_seconds: Histogram,
1636 /// `decdn_serve_first_frame_hit_seconds`: the first-frame phase of a paid
1637 /// hit, from the `StreamResponse` written to the first `ChunkData` frame
1638 /// written. It covers the read and encode of the whole first frame.
1639 #[default(Histogram::new(FIRST_BYTE_BUCKETS.to_vec()))]
1640 pub serve_first_frame_hit_seconds: Histogram,
1641 /// `decdn_serve_first_frame_miss_seconds`: the first-frame phase of a paid
1642 /// miss, from the `StreamResponse` written to the first `ChunkData` frame
1643 /// written. On a window-paced fill it covers the pull leg filling the whole
1644 /// first frame from upstream or origin.
1645 #[default(Histogram::new(FIRST_BYTE_BUCKETS.to_vec()))]
1646 pub serve_first_frame_miss_seconds: Histogram,
1647 /// `decdn_origin_directory_get_origins_failures_total`: `getOrigins`
1648 /// lookups that failed on a cold-namespace cache miss. The directory fails
1649 /// closed on each (resolves no origins for that request, does not cache
1650 /// the failure), so this is the drift signal to alert on — sustained
1651 /// failures mean real requests are silently losing their origin fallback.
1652 pub origin_directory_get_origins_failures: Counter,
1653 /// `decdn_origin_directory_cache_size`: current namespace count held in
1654 /// the lazy origin directory cache (positive + negative entries).
1655 /// Bounded by the configured cache capacity (LRU eviction).
1656 pub origin_directory_cache_size: Gauge,
1657 /// Paid-delivery (`serve_stream`) requests refused because the blob was
1658 /// deliberately evicted between probe and stream (#279). One `Counter` per
1659 /// reason — like the `dispatch_rejected_*` convention — because a plain counter
1660 /// field carries no label dimension, and the wire `StreamError` deliberately
1661 /// conflates the three `NotFound` reasons (`cache_miss`, `unknown_lane`,
1662 /// `owner_mismatch`) (#876). Visible name:
1663 /// `decdn_serve_stream_rejected_evicted_since_probe_total`.
1664 pub serve_stream_rejected_evicted_since_probe: Counter,
1665 /// `serve_stream` requests refused because the blob is absent and
1666 /// pull-through was unavailable or failed to fill it. Visible name:
1667 /// `decdn_serve_stream_rejected_cache_miss_total`.
1668 pub serve_stream_rejected_cache_miss: Counter,
1669 /// `serve_stream` requests refused by a local store fault (`has`/`inspect`
1670 /// error, or a present blob reporting no size) — signed to the client as
1671 /// `Declined`, not a signed absence. Visible name:
1672 /// `decdn_serve_stream_rejected_internal_error_total`.
1673 pub serve_stream_rejected_internal_error: Counter,
1674 /// `serve_stream` requests refused on an unknown / never-opened lane
1675 /// (#848). Wire-indistinguishable from `cache_miss`/`owner_mismatch` (all
1676 /// signed as `NotFound` to avoid leaking lane existence), so this
1677 /// server-side counter is the only place the distinction lives — a rising
1678 /// value isolates an unknown-lane abuse campaign. Visible name:
1679 /// `decdn_serve_stream_rejected_unknown_lane_total`.
1680 pub serve_stream_rejected_unknown_lane: Counter,
1681 /// `serve_stream` requests refused because a verified client binding does
1682 /// not authorize the named channel (#327). Visible name:
1683 /// `decdn_serve_stream_rejected_owner_mismatch_total`.
1684 pub serve_stream_rejected_owner_mismatch: Counter,
1685 /// `serve_stream` requests reset because the client binding failed to
1686 /// verify: the signature is invalid, or it recovers a different address
1687 /// than the one claimed. No signed response is sent. Visible name:
1688 /// `decdn_serve_stream_rejected_bad_binding_total`.
1689 pub serve_stream_rejected_bad_binding: Counter,
1690 /// Probe requests this node could not read: the read timed out, the frame
1691 /// or the message failed to decode, or the peer sent a response on the
1692 /// server stream. The connection closes with the matching ADR 013 code.
1693 /// Visible name: `decdn_probe_read_faults_total`.
1694 pub probe_read_faults: Counter,
1695 /// `serve_stream` requests refused before any bytes are served because the
1696 /// requesting channel's remaining deposit could not cover what the node
1697 /// would front at its rate. (The refusal itself is signed — as an `ok: false`
1698 /// response; what is never signed is an `ok: true`.) **Three** guards bump
1699 /// this. Not a sequence any one request walks: a cache HIT reaches only (3),
1700 /// and the window tier returns out of `serve_stream`, so (2) and (3) are
1701 /// mutually exclusive. A miss passes (1) and then at most one of (2)/(3):
1702 /// 1. the cache-miss **floor** (#1519) — one credit window, applied above
1703 /// every fill tier so none of them fronts origin egress or upstream USDC
1704 /// for a channel that cannot pay for a single interval;
1705 /// 2. the **window** tier's speculative ceiling (#856) — the whole-blob cost
1706 /// when `max_blob_size_bytes` is finite, else the window cost;
1707 /// 3. the **direct-serve** ceiling (#1516) —
1708 /// `min(credit window, chunk-group-aligned request span)`.
1709 ///
1710 /// It does not distinguish them, so a spike cannot be attributed to a layer
1711 /// from this series alone — the log line at the refusal site is what says
1712 /// which. Guards (1) and (3) also split by CAP: both run the two-level floor
1713 /// check, and its per-signer arm bumps `serve_stream_rejected_signer_floor_at_cap`
1714 /// instead, so this counter is the pool-ceiling half of them.
1715 /// Signed as `Unfunded`: every guard runs past the lane-ownership proof, so
1716 /// its audience is the proven requester (ADR 005 §Open-time refusal classes).
1717 /// Visible name: `decdn_serve_stream_rejected_insufficient_deposit_total`.
1718 pub serve_stream_rejected_insufficient_deposit: Counter,
1719 /// Delivery refused at admission because a wired `PoolView` could not confirm
1720 /// the pool on-chain — the pool has no on-chain record, is closed/reclaimed, or
1721 /// the admit-path `getPool` faulted — or could not confirm the voucher signer:
1722 /// the admit-path `getAuthorization` faulted and the node holds no cached
1723 /// registered read of the signer. Signed as `NotFound`, like a cache miss,
1724 /// so this counter is the only place the reason lives;
1725 /// kept distinct from `insufficient_deposit` so a real drained-pool refusal is
1726 /// not conflated with an unconfirmed or unreachable pool. Visible name:
1727 /// `decdn_serve_stream_rejected_pool_unconfirmed_total`.
1728 pub serve_stream_rejected_pool_unconfirmed: Counter,
1729 /// Delivery refused at admission because the request's pool is `Closing`
1730 /// on-chain: it takes no top-up, and its redemption ends at the dispute
1731 /// deadline (ADR 003 §Pool solvency). Signed as `Unfunded` to a requester
1732 /// that holds a lane on the pool, so its owner opens a new pool, and as
1733 /// `NotFound` to anyone else. Visible name:
1734 /// `decdn_serve_stream_rejected_pool_closing_total`.
1735 pub serve_stream_rejected_pool_closing: Counter,
1736 /// Delivery refused at admission because the request's voucher signer
1737 /// cannot pay: it is registered on-chain with `cap − spent` below a serve
1738 /// floor or with an expired registration (a "spent" capability the node
1739 /// could never cash), or its lane's capability is inside the node's expiry
1740 /// margin. A `getAuthorization` fault never counts here (see
1741 /// `pool_unconfirmed`). Signed as `Unfunded`: the requester holds a verified
1742 /// binding for a registered signer or a lane, so it is proven (ADR 005
1743 /// §Open-time refusal classes). A rising value flags capabilities presented
1744 /// whose signer has drained its shared `cap` at other nodes. Visible name:
1745 /// `decdn_serve_stream_rejected_signer_cap_exhausted_total`.
1746 pub serve_stream_rejected_signer_cap_exhausted: Counter,
1747 /// A live serve was stopped MID-STREAM because the request's voucher signer
1748 /// drained its shared on-chain `cap` at other nodes AFTER admission — the
1749 /// event-fed projection's `signer_spent` rose until the `cap − spent` headroom
1750 /// the node holds on the lane no longer covers a serve floor (ADR 003 §Pool
1751 /// solvency). The mid-stream twin of `serve_stream_rejected_signer_cap_exhausted`
1752 /// (which is the admit-time refusal), kept distinct so a running value flags
1753 /// cross-node cap drain on long streams — the large-blob over-delivery this
1754 /// re-check bounds — rather than an open-time refusal. The stream stops in-band
1755 /// with a `VoucherRejected { SignerCapExhausted }`, which the requester treats
1756 /// as `Unfunded` from this node. Visible name:
1757 /// `decdn_serve_stream_midstream_signer_cap_exhausted_total`.
1758 pub serve_stream_midstream_signer_cap_exhausted: Counter,
1759 /// Delivery refused because ONE capability signer hit its per-signer live
1760 /// concurrency cap of un-vouchered reservation, while the pool itself can still
1761 /// pay (ADR 003 §Pool solvency, per-signer floor isolation).
1762 /// Signed as `NotFound`, because it clears by waiting rather than by funding
1763 /// (ADR 005 §Open-time refusal classes), so this counter is the only place
1764 /// the reason survives — a rising value means one signer runs
1765 /// more concurrent un-vouchered streams than its share covers while the pool as a
1766 /// whole is solvent. The two remedies differ: a pool shortfall clears with a
1767 /// top-up, a signer at its share does not. Visible name:
1768 /// `decdn_serve_stream_rejected_signer_floor_at_cap_total`.
1769 pub serve_stream_rejected_signer_floor_at_cap: Counter,
1770 /// Whole-blob requests from an active staker refused while this node's own
1771 /// upstream open for the blob was in progress (#2224): the guard that stops
1772 /// partial holders pulling a blob from each other in a loop. Signed as
1773 /// `NotFound`, so this counter is the only place the reason lives. A rate
1774 /// that tracks node-to-node pulls of one blob shows the guard breaking loops;
1775 /// the debug line `refusing a whole-blob request from a node` names the
1776 /// requester. Visible name: `decdn_serve_stream_rejected_pull_loop_guard_total`.
1777 pub serve_stream_rejected_pull_loop_guard: Counter,
1778 /// Delivery refused because the requested bounded range starts at or past
1779 /// this node's size claim for the blob (ADR 005 §Bounded byte ranges).
1780 /// Signed as `Declined`: the size is this node's claim, and another node
1781 /// can disagree. A rising value flags clients issuing ranges past the end,
1782 /// or nodes that disagree on a blob's size. Visible name:
1783 /// `decdn_serve_stream_rejected_range_not_satisfiable_total`.
1784 pub serve_stream_rejected_range_not_satisfiable: Counter,
1785 /// Delivery refused because a pull-through request starts at or past this
1786 /// node's `max_blob_size` ceiling (ADR 005 §`max_blob_size` enforcement).
1787 /// The node refuses before it commits to the stream, so no byte is bought
1788 /// upstream. Signed as `Declined`. A rising value means clients ask
1789 /// this node for blobs above its ceiling. Visible name:
1790 /// `decdn_serve_stream_rejected_blob_too_large_total`.
1791 pub serve_stream_rejected_blob_too_large: Counter,
1792 /// Delivery refused because the blob is on this operator's local denylist
1793 /// (ADR 011 §Local Denylist). Signed as `Declined`. Deliberately
1794 /// counts ONLY the local list; the governance blacklist has its own
1795 /// counter, [`Self::serve_stream_rejected_chain_hash_denied`]. Splitting
1796 /// them here is safe where the wire code must not: this is the operator's
1797 /// own gauge of their own denylist, not something a client can probe. A
1798 /// rising value after a takedown is the confirmation the order is being
1799 /// discharged. Visible name:
1800 /// `decdn_serve_stream_rejected_hash_denied_total`.
1801 pub serve_stream_rejected_hash_denied: Counter,
1802 /// Delivery refused because the blob is on the *governance* blacklist (ADR
1803 /// 011 §On Blacklist Event). Also signed as `Declined` — identically
1804 /// to the local list, which is the ADR's requirement — so this counter is
1805 /// the only place the two are distinguishable, and it is readable by the
1806 /// operator alone. Visible name:
1807 /// `decdn_serve_stream_rejected_chain_hash_denied_total`.
1808 ///
1809 /// Not to be confused with `serve_stream_rejected_evicted_since_probe`,
1810 /// which sees only evictions with no blacklist entry behind them (corruption
1811 /// recovery, a manual `decdn node evict`).
1812 pub serve_stream_rejected_chain_hash_denied: Counter,
1813 /// Delivery refused because the channel's funding address is blacklisted as
1814 /// an origin — local `denied_origins` or the on-chain `ContentBlacklist`
1815 /// (ADR 011 §On Blacklist Event). Signed as `Declined`. Visible
1816 /// name: `decdn_serve_stream_rejected_origin_denied_total`.
1817 pub serve_stream_rejected_origin_denied: Counter,
1818 /// Delivery declined by the origin-only policy (#1759,
1819 /// `cache.relay_foreign_namespaces = false`): a memoized live probe of this
1820 /// node's own backend genuinely does not hold the hash. Signed as
1821 /// `Declined`: this node will not relay the hash, and another node may
1822 /// (ADR 005 §Open-time refusal classes). Distinct from a backend FAULT
1823 /// during that same probe, which is never counted here: a fault is not an
1824 /// absence and lands in
1825 /// [`Self::serve_stream_rejected_internal_error`] instead. Visible name:
1826 /// `decdn_serve_stream_rejected_foreign_declined_total`.
1827 pub serve_stream_rejected_foreign_declined: Counter,
1828 /// A serve refused because the node has been unable to reach the chain for
1829 /// longer than `blockchain.chain_staleness_grace_sec` (ADR 011 § Serving
1830 /// while chain-stale): its deny-set, pool-solvency, and signer-cap guards
1831 /// are all reading stale state, so it refuses rather than sign a serve it
1832 /// cannot vouch for. Signed as `NotFound`, identically to
1833 /// [`Self::serve_stream_rejected_cache_miss`] — the client should re-route
1834 /// to a peer whose chain reads are live — so this counter is the only place
1835 /// an operator sees the staleness refusals. Visible name:
1836 /// `decdn_serve_stream_rejected_chain_stale_total`.
1837 pub serve_stream_rejected_chain_stale: Counter,
1838 /// An ALREADY-RUNNING delivery cut off at an MB boundary because a takedown
1839 /// landed after the stream opened (ADR 011 §On Blacklist Event). Visible
1840 /// name: `decdn_serve_stream_terminated_takedown_total`.
1841 ///
1842 /// Distinct from the `rejected_*` family above, which counts refusals at
1843 /// stream open. This one is the operator's evidence that the compliance
1844 /// window was honored for traffic already in flight — the case that would
1845 /// otherwise keep a multi-GB blob flowing for minutes after the order took
1846 /// effect, which is the slashable one.
1847 pub serve_stream_terminated_takedown: Counter,
1848 /// `decdn_serve_stream_rejected_load_shed_hit_total`: cache-hit serves shed
1849 /// under node overload (egress saturation).
1850 pub serve_stream_rejected_load_shed_hit: Counter,
1851 /// `decdn_serve_stream_rejected_load_shed_miss_total`: cache-miss serves shed
1852 /// under node overload (concurrency pressure or per-client fairness).
1853 pub serve_stream_rejected_load_shed_miss: Counter,
1854 /// `decdn_serve_frame_accounting_fault_total`: a serve leg refused to cut or
1855 /// frame a `ChunkData` because its own byte accounting did not add up — a zero
1856 /// frame target, a `queued`/`queue` desync, a payload whose chunks disagree with
1857 /// the length the header would declare, or a header the encoder rejected.
1858 ///
1859 /// Every one of those is a node-side bug, never peer behaviour, and each aborts
1860 /// the delivery without `StreamEnd` so the client neither receives a mislabelled
1861 /// frame nor pays the closing voucher. The refusals are correct; this counter
1862 /// exists because they are otherwise invisible — the stream simply ends, which
1863 /// reads to an operator as a client that hung up. Any nonzero value is a
1864 /// **latent-bug report, not a degradation**: alert on `> 0` and file it rather
1865 /// than tuning anything.
1866 pub serve_frame_accounting_fault: Counter,
1867 /// `decdn_serve_stream_node_fault_total`: a `cdn/client/v1` delivery ended on a
1868 /// fault this node caused — an encode fault, an alignment error, a store fault,
1869 /// a framing fault — rather than on a peer hang-up or a client payment fault.
1870 ///
1871 /// The client sees only a short stream, so without this counter the whole class
1872 /// is visible solely in the `error!` log line beside it. Any nonzero value is a
1873 /// **latent-bug report, not a degradation**: alert on `> 0` and file it. It is a
1874 /// superset of `decdn_serve_frame_accounting_fault_total`, which meters one of
1875 /// the four classes on its own.
1876 pub serve_stream_node_fault: Counter,
1877 /// `decdn_serve_stream_proof_budget_exhausted_total`: a `cdn/client/v1`
1878 /// delivery ended because the payer sent `MAX_PROOFS_PER_CHUNK` proofs for one
1879 /// chunk and none of them settled it. Counted for both the cache-hit and the
1880 /// miss serve loop; on the miss loop the same exit also counts on
1881 /// `decdn_node_pull_through_client_abandoned_total`.
1882 ///
1883 /// Each such proof credits part of the chunk or nothing, so a payer that
1884 /// answers one chunk with zero-credit vouchers or partial payments ends here.
1885 /// This budget is what stops such a payer from holding a stream open (#2132).
1886 /// It is the payer's fault, not a node fault, so the serve loop logs it only
1887 /// at `debug!` and ends the stream as a stop; this counter is what makes the
1888 /// rate visible at the default log level. Each bump ends exactly one inbound
1889 /// stream as failed.
1890 pub serve_stream_proof_budget_exhausted: Counter,
1891 /// `decdn_serve_stream_rejected_stream_cap_full_total`: `cdn/client/v1`
1892 /// streams reset with `RATE_LIMITED` because the connection already had
1893 /// `max_concurrent_streams` streams in flight. No signed response is sent.
1894 /// A client that opens more concurrent streams than the cap drives it.
1895 pub serve_stream_rejected_stream_cap_full: Counter,
1896 /// `decdn_serve_stream_request_unreadable_total`: `cdn/client/v1` streams
1897 /// reset because the node could not read the first request: the read timed
1898 /// out, the peer reset the stream or dropped the connection, or the frame or
1899 /// its extensions failed to decode. Peer-side, logged at `debug!`.
1900 pub serve_stream_request_unreadable: Counter,
1901 /// `decdn_serve_stream_voucher_rejected_total`: paying `cdn/client/v1`
1902 /// streams the node stopped because it rejected a voucher or a chunk
1903 /// preimage: for example a wrong pool or signer, a bad signature, an
1904 /// underpayment, a regression, an expired capability, or a voucher that fails
1905 /// the rate check. Counted for both the cache-hit and the miss serve loop; on
1906 /// the miss loop the same exit also counts on
1907 /// `decdn_node_pull_through_client_abandoned_total`. A rejection counts here
1908 /// even when the peer leaves before it reads the reject frame.
1909 pub serve_stream_voucher_rejected: Counter,
1910 /// `decdn_serve_stream_client_declined_total`: `cdn/client/v1` streams the
1911 /// requester left before any voucher credited a byte. The routine shapes are a
1912 /// requester that reads the signed `StreamResponse` and does not take the
1913 /// quote, and the header handshake a downstream node's miss pull opens and
1914 /// closes without adopting. A peer that sends a malformed proof before paying
1915 /// counts here too. The reference requester closes every stream with code `0`,
1916 /// and the node does not read the code, so it sees only that the peer left,
1917 /// not why. Peer-side, logged at `debug!`.
1918 pub serve_stream_client_declined: Counter,
1919 /// `decdn_serve_stream_client_abandoned_total`: paying `cdn/client/v1`
1920 /// streams the requester left or broke the protocol on after at least one
1921 /// voucher credited bytes: it stopped reading, reset the stream, dropped the
1922 /// connection, stopped sending proofs, or sent a malformed proof. A fully paid
1923 /// stream whose `StreamEnd` write fails counts here too. Peer-side, logged at `debug!`. A sustained rise against
1924 /// `decdn_streams_completed_total{direction="inbound"}` flags clients that
1925 /// give up mid-delivery.
1926 pub serve_stream_client_abandoned: Counter,
1927 /// `decdn_load_shed_egress_bps`: current measured egress EWMA, bytes/sec.
1928 pub load_shed_egress_bps: Gauge,
1929 /// `decdn_load_shed_pressure_active`: 1 while the load-shed policy considers
1930 /// the node pressured, else 0.
1931 pub load_shed_pressure_active: Gauge,
1932 /// `decdn_load_shed_streams_in_flight`: node-wide serves in flight, sampled
1933 /// from the shed controller's live counter beside the egress EWMA. Read it
1934 /// against the configured high-water mark to see how close the node is to
1935 /// shedding on concurrency — the `pressure_active` gauge only says whether
1936 /// the mark is crossed, not the headroom.
1937 pub load_shed_streams_in_flight: Gauge,
1938 /// `decdn_load_shed_refused_node_at_capacity_total`: new serves shed because
1939 /// node-wide concurrency was above the high-water mark. The
1940 /// [`crate::load_shed::ShedReason::NodeAtCapacity`] sibling — per
1941 /// `adr/appendix-observability.md` § Reason splits each shed reason is a
1942 /// sibling counter, because the remedy differs: this one says add capacity or
1943 /// raise `max_concurrent_serves_high`. Orthogonal to the hit/miss split in
1944 /// `serve_stream_rejected_load_shed_{hit,miss}`, which counts the same
1945 /// refusals by cache class.
1946 pub load_shed_refused_node_at_capacity: Counter,
1947 /// `decdn_load_shed_refused_egress_saturated_total`: new serves shed because
1948 /// measured egress reached the configured budget
1949 /// ([`crate::load_shed::ShedReason::EgressSaturated`]). Remedy: raise
1950 /// `egress_budget_mbps` or add bandwidth. See
1951 /// [`Self::load_shed_refused_node_at_capacity`].
1952 pub load_shed_refused_egress_saturated: Counter,
1953 /// `decdn_load_shed_refused_client_at_capacity_total`: new serves shed
1954 /// because one client already held its fair share while the node was
1955 /// pressured ([`crate::load_shed::ShedReason::ClientAtCapacity`]). This is
1956 /// fairness working as designed, not node distress — remedy is usually none.
1957 /// See [`Self::load_shed_refused_node_at_capacity`].
1958 pub load_shed_refused_client_at_capacity: Counter,
1959 /// `decdn_warming_speculative_blocked_total` (ADR 041): times an above-floor
1960 /// (speculative) warming buy was downgraded to the amortized floor because
1961 /// the upstream source's per-source allowance was spent. Counted only when
1962 /// warming is enabled (`warming_budget > 0`), so a node with warming off does
1963 /// not report every source as blocked. A sustained rate means one or more
1964 /// sources are being griefed down — pair it with
1965 /// [`Self::warming_sources_blocked`] for how many.
1966 pub warming_speculative_blocked: Counter,
1967 /// `decdn_warming_sources_blocked` (ADR 041): how many upstream sources
1968 /// currently hold no warming allowance (spent ledger, refill projected
1969 /// forward), sampled beside the egress EWMA. A rising gauge with a rising
1970 /// [`Self::warming_speculative_blocked`] is warming being throttled by real
1971 /// per-source losses; either at zero means warming is unconstrained.
1972 pub warming_sources_blocked: Gauge,
1973
1974 // ---- Uniform watcher liveness + panic surface (#1316, #1320) ----
1975 //
1976 // The `*_down_seconds` family below is edge-triggered off a tick *error*, so
1977 // a watcher that panicked while healthy, wedged in an await, or exited
1978 // cleanly leaves `down_since == None` and reads a healthy `0` forever — a
1979 // dead watcher is byte-identical to a live one. These two families close
1980 // that gap for all five chain-event watchers: `*_last_tick_timestamp_seconds`
1981 // is a *positive* liveness signal a dead task cannot advance, and
1982 // `*_task_panicked_total` makes an otherwise-discarded task panic visible.
1983 /// `decdn_slash_watcher_last_tick_timestamp_seconds` (#1316): Unix time of
1984 /// the slash watcher's last successful poll tick, stamped every tick (not
1985 /// edge-triggered). Alert on staleness — `time() - value > 3 ×
1986 /// poll_interval` — to catch a panicked, wedged, or cleanly-exited task that
1987 /// `slash_watcher_down_seconds` (error-triggered) cannot. `0` until the
1988 /// first successful tick.
1989 pub slash_watcher_last_tick_timestamp_seconds: Gauge,
1990 /// `decdn_staker_set_watcher_last_tick_timestamp_seconds` (#1316): Unix time
1991 /// of the staker-set watcher's last successful poll tick. Staleness
1992 /// semantics as `slash_watcher_last_tick_timestamp_seconds`.
1993 pub staker_set_watcher_last_tick_timestamp_seconds: Gauge,
1994 /// `decdn_capacity_bond_registry_last_resync_timestamp_seconds`: Unix time
1995 /// of the capacity-bond registry's last successful re-enumeration of
1996 /// `getRegisteredNodes`.
1997 ///
1998 /// Distinct from the tick gauge above: a tick is the event tail, this is the
1999 /// authoritative re-read that repairs a drifted set. It is the only signal
2000 /// that catches a resync being *skipped* — the reconcile does not run at all
2001 /// while the route is errored, which emits nothing, not even the failure
2002 /// counter. Stamped by the bootstrap enumeration too, which is the same read
2003 /// against the same contract; without that a node whose every later resync
2004 /// fails would hold the gauge at `0` and never trip a `> 0`-guarded alert.
2005 /// The guard is still needed for the window before bootstrap completes, and
2006 /// the threshold must exceed `REGISTRY_RESYNC_INTERVAL`.
2007 pub capacity_bond_registry_last_resync_timestamp_seconds: Gauge,
2008 /// `decdn_blacklist_watcher_last_tick_timestamp_seconds` (#1316, #1320): Unix
2009 /// time of the blacklist watcher's last successful poll tick. This is the
2010 /// signal that distinguishes a live blacklist loop from a dead one — a dead
2011 /// loop keeps serving content blacklisted after the failure (slashable via
2012 /// `SlashJudge.submitBlacklistChallenge`) while `blacklist_watcher_down_seconds`
2013 /// reads `0`.
2014 pub blacklist_watcher_last_tick_timestamp_seconds: Gauge,
2015 /// `decdn_settlement_watcher_last_tick_timestamp_seconds` (#1316): Unix time
2016 /// of the payment-settlement watcher's last successful poll tick.
2017 pub settlement_watcher_last_tick_timestamp_seconds: Gauge,
2018 /// `decdn_fee_shares_watcher_last_tick_timestamp_seconds`: Unix time of the
2019 /// fee-shares watcher's last successful poll tick. Load-bearing: the
2020 /// watcher's `getShares()` poll failure and undecodable-log paths both
2021 /// return `Ok` by design, so a watcher stuck in RPC backoff — or dead — is
2022 /// otherwise indistinguishable from a healthy one while the node keeps
2023 /// signing quotes against a stale operator fee share. Alert on this gauge
2024 /// going stale.
2025 pub fee_shares_watcher_last_tick_timestamp_seconds: Gauge,
2026 /// `decdn_slash_watcher_task_panicked_total` (#1316): the slash watcher task
2027 /// unwound on a panic. Bumped from a `Drop` guard in `multiplexed_poller::run`
2028 /// — the only thing that still runs on the unwind, since nothing awaits the
2029 /// detached task. Any non-zero value is a bug in this node.
2030 pub slash_watcher_task_panicked: Counter,
2031 /// `decdn_staker_set_watcher_task_panicked_total` (#1316): the staker-set
2032 /// watcher task unwound on a panic. See `slash_watcher_task_panicked`.
2033 pub staker_set_watcher_task_panicked: Counter,
2034 /// `decdn_blacklist_watcher_task_panicked_total` (#1316, #1283): the
2035 /// blacklist watcher task unwound on a panic.
2036 pub blacklist_watcher_task_panicked: Counter,
2037 /// `decdn_fee_shares_watcher_task_panicked_total`: the fee-shares watcher
2038 /// task unwound on a panic. Any non-zero value is a bug in this node.
2039 pub fee_shares_watcher_task_panicked: Counter,
2040 /// `decdn_settlement_watcher_task_panicked_total` (#1316): the
2041 /// payment-settlement watcher task unwound on a panic.
2042 pub settlement_watcher_task_panicked: Counter,
2043
2044 // ---- Shared chain-event poller: eth_getLogs window span ----
2045 /// `decdn_chain_get_logs_span`: block span of the chain-event poller's next
2046 /// `eth_getLogs` window. It starts at `blockchain.get_logs_max_block_span`,
2047 /// drops to half a window the RPC provider rejects for its range, and
2048 /// doubles back after 32 accepted windows, up to the ceiling. A caught-up
2049 /// node climbs back to the ceiling after a transient rejection, so a low
2050 /// value is a recent rejection, not proof of a cap: read the rejection
2051 /// counter for that. The lowest values over a range with rejections show
2052 /// the provider's cap.
2053 pub chain_get_logs_span: Gauge,
2054 /// `decdn_chain_get_logs_range_rejections_total`: `eth_getLogs` windows the
2055 /// RPC provider rejected for their block range or result count. Each one
2056 /// costs one wasted request. Rejections that keep coming mean the provider
2057 /// caps `eth_getLogs`; set `blockchain.get_logs_max_block_span` to the
2058 /// lowest span the gauge reaches while they come, and they stop.
2059 pub chain_get_logs_range_rejections: Counter,
2060 /// `decdn_chain_get_logs_retries_total` (#2161): in-tick retries of
2061 /// `eth_getLogs` windows after a transient provider error. One window adds
2062 /// up to two, including a window that fails every retry and is deferred
2063 /// (`decdn_chain_get_logs_deferred_total`). A retry that succeeds does not
2064 /// count as watcher downtime, so a provider that the retries absorb shows
2065 /// here and not in the watchers' down-family. Rate limits and permanent
2066 /// errors are not retried.
2067 pub chain_get_logs_retries: Counter,
2068 /// `decdn_chain_get_logs_deferred_total` (#2253): `eth_getLogs` windows
2069 /// that failed every in-tick retry, so the poller ended the tick there and
2070 /// resumed the window on the next tick. A deferral is not watcher downtime;
2071 /// the tick fails, and the watchers go down, only when no tick has reached
2072 /// head for the stall budget (2 min).
2073 pub chain_get_logs_deferred: Counter,
2074 /// `decdn_chain_boot_read_retries_total` (#2159): retries of a boot-time
2075 /// chain read (the registry, slash, `usdc()` self-check and blacklist
2076 /// bootstraps, and the best-effort fee-share reads) after a transient
2077 /// error; one read can retry many times. The metrics
2078 /// listener binds after these reads, so the value becomes visible once boot
2079 /// completes. A rise after a restart points at the RPC provider.
2080 pub chain_boot_read_retries: Counter,
2081
2082 // ---- Down-family parity for the watchers that lacked it (#1283, #1316) ----
2083 /// `decdn_blacklist_watcher_restarts_total` (#1283): distinct drift windows
2084 /// the blacklist watcher entered, bumped once on the edge into the
2085 /// error/backoff state. Mirrors `slash_watcher_restarts`.
2086 pub blacklist_watcher_restarts: Counter,
2087 /// `decdn_blacklist_watcher_down_seconds` (#1283): seconds the blacklist
2088 /// watcher has been failing its chain read (`0` on a healthy cycle),
2089 /// recomputed at scrape from `blacklist_watcher_down_since`. Tracks
2090 /// chain-read outages only — an enforcement failure that leaves a blacklisted
2091 /// blob servable is a separate signal (`blacklist_enforcement_failures`).
2092 pub blacklist_watcher_down_seconds: Gauge,
2093 /// `decdn_settlement_watcher_restarts_total` (#1316): distinct drift windows
2094 /// the payment-settlement watcher entered. Mirrors `slash_watcher_restarts`.
2095 pub settlement_watcher_restarts: Counter,
2096 /// `decdn_settlement_watcher_down_seconds` (#1316): seconds the settlement
2097 /// watcher has been failing its chain read, recomputed at scrape from
2098 /// `settlement_watcher_down_since`.
2099 pub settlement_watcher_down_seconds: Gauge,
2100
2101 /// `decdn_blacklist_enforcement_failures_total` (#1319): distinct hashes a
2102 /// batched re-scope could NOT re-verify or evict this pass (`Recheck::Failed`
2103 /// — a disk error or a scope `eth_call` failure). An increase means a pass
2104 /// did not fully enforce the deny-set, so a blacklisted blob may still be
2105 /// servable and slashable, even while `blacklist_watcher_down_seconds` reads
2106 /// `0`. Answers a different question than the down-family, which tracks
2107 /// chain-read outages only. A boot that retries its enforcement pass also
2108 /// adds to it; the node serves only after a clean pass, so that increase does
2109 /// not mean a served blob. Pairs with the aggregate `warn!` in `rescan`.
2110 pub blacklist_enforcement_failures: Counter,
2111 /// `decdn_blacklist_reenumeration_failures_total`: times the blacklist
2112 /// watcher's periodic re-enumeration could not read chain state and kept the
2113 /// deny-set it already had. That re-enumeration is the backstop for a lost
2114 /// tail event and for a region/ripening transition, which emits no event.
2115 /// It reports success upward so the event tail keeps running, so a
2116 /// persistently failing backstop moves nothing else. Pairs with the `warn!`
2117 /// in `blacklist_watcher`'s `BlacklistSink::on_tick_complete`.
2118 pub blacklist_reenumeration_failures: Counter,
2119
2120 // ---- Stream outcomes and volume ----
2121 /// `decdn_streams_completed_total{direction}`: paid streams that delivered
2122 /// the whole request. `inbound` counts streams this node served. `outbound`
2123 /// counts node-to-node pulls this node made: one per upstream candidate it
2124 /// opened a stream to on the buffered miss path, one per assembled range on
2125 /// the streaming miss path. Every ended stream counts once, in this family
2126 /// or in `streams_failed`; a stream cancelled by shutdown counts in
2127 /// neither. The reason split lives in the sibling counters
2128 /// (`serve_stream_rejected_*`, `node_pull_*`).
2129 streams_completed: Family<StreamLabels, Counter>,
2130 /// `decdn_streams_failed_total{direction}`: paid streams that ended without
2131 /// delivering the whole request — refused, stopped mid-stream, reset,
2132 /// panicked, or ended on an error. Routine outcomes count here too: a cache
2133 /// miss refusal, a requester that declines the quote — including the header
2134 /// handshake a downstream node's miss pull opens and does not adopt — and a
2135 /// client that leaves mid-stream. So read the reason siblings, not a raw
2136 /// ratio, for health. See `streams_completed`.
2137 ///
2138 /// Every `inbound` failure counts on exactly one sibling in
2139 /// [`INBOUND_FAILURE_REASONS`], so `inbound` minus their sum is zero. A
2140 /// positive residual is a failure no reason claims, which is a bug.
2141 streams_failed: Family<StreamLabels, Counter>,
2142 /// `decdn_bytes_served_total`: payload bytes this node wrote to clients and
2143 /// downstream nodes on `cdn/client/v1`, counted per frame as it is written.
2144 pub bytes_served: Counter,
2145 /// `decdn_bytes_received_total`: payload bytes this node admitted from
2146 /// upstream nodes in paid node-to-node pulls, counted per verified range
2147 /// (chunk-group aligned, so it can exceed the requested bytes).
2148 pub bytes_received: Counter,
2149
2150 // ---- Settlement and on-chain transactions ----
2151 /// `decdn_pool_redemptions_total`: lane claims this node redeemed on-chain in
2152 /// a landed `redeemMany` — one cumulative voucher per lane, however many
2153 /// vouchers the lane accepted.
2154 pub pool_redemptions: Counter,
2155 /// `decdn_vouchers_received_total`: signed vouchers this node accepted as
2156 /// the payee on an inbound stream. `PayWord` preimage reveals are not
2157 /// vouchers and do not count; a `PayWord` stream counts its anchor voucher.
2158 pub vouchers_received: Counter,
2159 /// `decdn_preimage_reveals_received_total`: `PayWord` hash-chain preimage
2160 /// reveals this node accepted as the payee on an inbound stream, counted
2161 /// when a reveal advances the lane's chain frontier. Each one also hints the
2162 /// redeemer, so this rate bounds the redeemer's hint rate beside
2163 /// `decdn_vouchers_received_total`.
2164 pub preimage_reveals_received: Counter,
2165 /// `decdn_pool_grace_closes_total`: pools that entered the owner-close grace
2166 /// window while this node still held unredeemed vouchers on them, read from
2167 /// the flushed lane store when the close event arrives. Each one is revenue
2168 /// that must redeem before the grace window ends. A pool already closing at
2169 /// startup is not counted.
2170 pub pool_grace_closes: Counter,
2171 /// `decdn_onchain_tx_landed_total`: settlement transactions (`redeemMany`)
2172 /// mined and succeeded. The outcome family — `landed`, `reverted`,
2173 /// `send_failed`, `receipt_failed`, `timeout` — counts every transaction
2174 /// sent through `send_and_await_receipt` exactly once;
2175 /// `onchain_tx_receipt_recovered` sits beside it and is not an outcome.
2176 /// Buyer-side pool transactions go through `decdn-client` and are not
2177 /// counted here.
2178 pub onchain_tx_landed: Counter,
2179 /// `decdn_onchain_tx_reverted_total`: node transactions mined and reverted.
2180 pub onchain_tx_reverted: Counter,
2181 /// `decdn_onchain_tx_send_failed_total`: node transactions the RPC refused at
2182 /// `send` — none was issued. An oversize `redeemMany` the redeemer then
2183 /// halves and retries counts here once too.
2184 pub onchain_tx_send_failed: Counter,
2185 /// `decdn_onchain_tx_receipt_failed_total`: issued node transactions whose
2186 /// receipt wait failed and whose receipt no by-hash fetch found. The
2187 /// transaction is unconfirmed and may still mine.
2188 pub onchain_tx_receipt_failed: Counter,
2189 /// `decdn_onchain_tx_timeout_total`: issued node transactions whose receipt
2190 /// did not arrive inside the caller's bound and whose receipt no by-hash
2191 /// fetch found. The transaction is unconfirmed and may still mine.
2192 pub onchain_tx_timeout: Counter,
2193 /// `decdn_onchain_tx_receipt_recovered_total`: node transactions whose
2194 /// receipt wait failed or timed out but whose receipt a by-hash fetch then
2195 /// found. Each one also counts once as landed or reverted, so this sits
2196 /// beside the outcome family, not inside it. A steady rate points at a
2197 /// lagging or load-balanced RPC provider.
2198 pub onchain_tx_receipt_recovered: Counter,
2199
2200 // ---- DHT ----
2201 /// `decdn_dht_findvalue_queries_total`: `FIND_VALUE` lookups this node ran
2202 /// to discover providers.
2203 pub dht_findvalue_queries: Counter,
2204 /// `decdn_dht_lookup_round_timeouts_total`: lookup rounds that hit the round
2205 /// timeout and aborted their in-flight RPCs.
2206 pub dht_lookup_round_timeouts: Counter,
2207 /// `decdn_dht_store_published_total`: DHT `STORE` records a peer accepted
2208 /// from this node.
2209 pub dht_store_published: Counter,
2210 /// `decdn_dht_routing_table_size`: distinct entries in the local Kademlia
2211 /// routing table, set after bootstrap and on every bucket-refresh tick.
2212 pub dht_routing_table_size: Gauge,
2213 /// `decdn_dht_bucket_refresh_failures_total`: bucket refreshes that failed:
2214 /// a `FIND_NODE` RPC error, a routing-table peer that is not a valid public
2215 /// key, or a panicked refresh task.
2216 pub dht_bucket_refresh_failures: Counter,
2217 /// `decdn_dht_bootstrap_find_node_failures_total`: bootstrap `FIND_NODE`
2218 /// RPCs against a seed that failed.
2219 pub dht_bootstrap_find_node_failures: Counter,
2220
2221 // ---- Fee-shares watcher parity ----
2222 /// `decdn_fee_shares_watcher_unregistered`: `1` when the startup
2223 /// `PaymentPool.feeRouter()` read failed, so the fee-shares watcher is not
2224 /// running and the operator share stays at the floor for the life of the
2225 /// process; `0` otherwise. The watcher's own down gauges read healthy in
2226 /// that state, since it never started.
2227 pub fee_shares_watcher_unregistered: Gauge,
2228 /// `decdn_fee_shares_watcher_poll_failures_total`: authoritative
2229 /// `getShares()` re-reads that failed. The watcher keeps the current share and
2230 /// its tick still succeeds, so this is the only signal that the safety-net
2231 /// re-read is not landing.
2232 pub fee_shares_watcher_poll_failures: Counter,
2233 /// `decdn_fee_shares_watcher_restarts_total`: distinct drift windows the
2234 /// fee-shares watcher entered. Mirrors `slash_watcher_restarts`.
2235 pub fee_shares_watcher_restarts: Counter,
2236 /// `decdn_fee_shares_watcher_down_seconds`: seconds the fee-shares watcher
2237 /// has been failing its chain read, recomputed at scrape from
2238 /// `fee_shares_watcher_down_since`.
2239 pub fee_shares_watcher_down_seconds: Gauge,
2240
2241 // ---- Operator-path failures ----
2242 /// `decdn_config_reload_failures_total`: SIGHUP or admin config reloads that
2243 /// failed. Sections committed before the failure stay applied; the rest keep
2244 /// their previous values.
2245 pub config_reload_failures: Counter,
2246 /// `decdn_receipt_write_failures_total`: download-receipt audit records the
2247 /// writer failed to persist (an I/O error or a panicked write task). Audit
2248 /// only; settlement is unaffected.
2249 pub receipt_write_failures: Counter,
2250 /// `decdn_serve_stream_midstream_pool_exhausted_total`: paying streams this
2251 /// node stopped mid-delivery because the pool could no longer fund the
2252 /// floor credit committed across its signers. The pool-level sibling of
2253 /// `serve_stream_midstream_signer_cap_exhausted`.
2254 pub serve_stream_midstream_pool_exhausted: Counter,
2255}
2256
2257/// Per-request JSON-RPC metrics for the node's chain provider, registered under
2258/// the same `decdn` prefix as [`DecdnMetrics`].
2259///
2260/// A group of its own because a labelled histogram cannot deserialize, and
2261/// [`DecdnMetrics`] derives `Deserialize`. The `crate::rpc_metrics` transport
2262/// layer records into it for every request of every provider the runtime
2263/// builds, so these are the denominator the provider's error ratio needs.
2264#[derive(Debug, MetricsGroup)]
2265#[metrics(default, name = "decdn")]
2266struct RpcMetrics {
2267 /// `decdn_rpc_requests_total{method,outcome}`: JSON-RPC requests the node's
2268 /// providers sent, by method and by how each ended. Each request of a batch
2269 /// counts once. Every `(method, outcome)` pair exports at zero from
2270 /// startup.
2271 ///
2272 /// Labelled rather than split into sibling counters because every outcome
2273 /// shares one aggregate: the error ratio is a sum over `outcome` divided by
2274 /// the total. See [`RpcOutcome`] for each value.
2275 rpc_requests: Family<RpcRequestLabels, Counter>,
2276 /// `decdn_rpc_request_duration_seconds{method}`: time from sending one
2277 /// JSON-RPC request to its reply or transport error. A request the caller
2278 /// drops records no sample. Buckets: [`RPC_REQUEST_BUCKETS`].
2279 #[default(Family::with_constructor(|| Histogram::new(RPC_REQUEST_BUCKETS.to_vec())))]
2280 rpc_request_duration_seconds: Family<RpcMethodLabels, Histogram>,
2281}
2282
2283/// Aggregated deCDN node metrics.
2284#[derive(Debug)]
2285pub struct Metrics {
2286 registry: Arc<RwLock<Registry>>,
2287 decdn: Arc<DecdnMetrics>,
2288 rpc: Arc<RpcMetrics>,
2289 cache: Arc<CacheMetrics>,
2290 inbound_streams: Arc<Gauge>,
2291 outbound_streams: Arc<Gauge>,
2292 inbound_completed: Arc<Counter>,
2293 inbound_failed: Arc<Counter>,
2294 outbound_completed: Arc<Counter>,
2295 outbound_failed: Arc<Counter>,
2296 /// Materialized `probe_hold_unavailable` children, one per
2297 /// [`ProbeHoldUnavailableReason`]. Held here for the same reason as the
2298 /// stream gauges: `Family` creates a child series lazily on first
2299 /// `get_or_create`, so without this each `reason` would be absent from
2300 /// `/metrics` until it first fired — an operator's dashboard and alert
2301 /// would show a gap rather than a zero. Creating them at startup keeps
2302 /// the property that all three series are exported at zero.
2303 probe_hold_exhausted: Arc<Counter>,
2304 probe_hold_disabled: Arc<Counter>,
2305 probe_hold_stake_lane_reserved: Arc<Counter>,
2306 /// Regions that currently have a `staker_set_active_by_region` child.
2307 /// `Family` cannot enumerate its children, so this is what tells
2308 /// [`Self::staker_set_active_by_region`] which ones to remove.
2309 published_regions: Mutex<BTreeSet<Region>>,
2310 started_at: Instant,
2311 /// Monotonic instant at which the staker-set watcher entered its current
2312 /// error/backoff window (#783, downtime semantics #788). `None` whenever a
2313 /// cycle is established (healthy) — including from bootstrap until the
2314 /// first cycle and after every successful re-establishment. `Some(t)` only
2315 /// while the loop is between a failed cycle and the next success. Backs the
2316 /// `staker_set_watcher_down_seconds` gauge, recomputed from this at scrape
2317 /// time so the value reads exactly `0` across an arbitrarily long healthy
2318 /// cycle and climbs only through an actual outage. A poisoned lock reports
2319 /// `i64::MAX` down-seconds (the conservative, alerting direction for a
2320 /// downtime gauge — reporting `0` would mask an in-progress outage).
2321 staker_set_watcher_down_since: Mutex<Option<Instant>>,
2322 /// `Instant` the slash-detection watcher entered its current error/backoff
2323 /// window (#1032). `None` whenever a cycle is established. Backs the
2324 /// `slash_watcher_down_seconds` gauge, recomputed at scrape time. Mirrors
2325 /// `staker_set_watcher_down_since`.
2326 slash_watcher_down_since: Mutex<Option<Instant>>,
2327 /// `Instant` the blacklist watcher entered its current error/backoff window
2328 /// (#1283). `None` whenever a cycle is established. Backs the
2329 /// `blacklist_watcher_down_seconds` gauge, recomputed at scrape time. Mirrors
2330 /// `staker_set_watcher_down_since`.
2331 blacklist_watcher_down_since: Mutex<Option<Instant>>,
2332 /// `Instant` the payment-settlement watcher entered its current error/backoff
2333 /// window (#1316). `None` whenever a cycle is established. Backs the
2334 /// `settlement_watcher_down_seconds` gauge. Mirrors
2335 /// `staker_set_watcher_down_since`.
2336 settlement_watcher_down_since: Mutex<Option<Instant>>,
2337 /// `Instant` the fee-shares watcher entered its current error/backoff
2338 /// window. `None` whenever a cycle is established. Backs the
2339 /// `fee_shares_watcher_down_seconds` gauge. Mirrors
2340 /// `staker_set_watcher_down_since`.
2341 fee_shares_watcher_down_since: Mutex<Option<Instant>>,
2342 /// Cache engine whose live probe-hold count backs the
2343 /// `probe_hold_slots_used` gauge, sampled once per scrape in
2344 /// [`Self::encode`]. Unset until runtime bring-up attaches the cache
2345 /// through [`Self::attach_probe_holds`]; the gauge then reads `0`.
2346 probe_holds: OnceLock<CacheEngine>,
2347}
2348
2349impl Default for Metrics {
2350 fn default() -> Self {
2351 Self::new()
2352 }
2353}
2354
2355/// Saturating `usize` → `i64` for gauge values: a count too large to fit an
2356/// `i64` reports `i64::MAX` rather than wrapping. Saturation is the
2357/// conservative direction for the sizes and counts these gauges carry, and it
2358/// keeps the workspace `unwrap_used` deny satisfied without pushing a
2359/// `Result` onto every recorder signature.
2360fn sat(n: usize) -> i64 {
2361 i64::try_from(n).unwrap_or(i64::MAX)
2362}
2363
2364/// Saturating `u64` → `i64` for gauge values; see [`sat`].
2365fn sat_u64(n: u64) -> i64 {
2366 i64::try_from(n).unwrap_or(i64::MAX)
2367}
2368
2369fn sat_u256(n: U256) -> i64 {
2370 u64::try_from(n)
2371 .ok()
2372 .and_then(|raw| i64::try_from(raw).ok())
2373 .unwrap_or(i64::MAX)
2374}
2375
2376/// Current wall-clock time as whole seconds since the Unix epoch, saturating.
2377///
2378/// This is deliberately `SystemTime` (wall clock), not the monotonic `Instant`
2379/// every other timer here uses: it backs the `*_last_tick_timestamp_seconds`
2380/// liveness gauges, whose alerting expression compares them against Prometheus's
2381/// `time()` (also wall clock). A pre-epoch clock (`Err`) reads `0` — i.e.
2382/// "never ticked", the alerting direction. Anti-panic: no `unwrap`/`expect`.
2383fn unix_now_secs() -> i64 {
2384 std::time::SystemTime::now()
2385 .duration_since(std::time::UNIX_EPOCH)
2386 .map_or(0, |d| i64::try_from(d.as_secs()).unwrap_or(i64::MAX))
2387}
2388
2389impl Metrics {
2390 /// Create the registry and register deCDN's metric group plus the
2391 /// cache crate's `decdn_cache_*` group. The cache handle is shared
2392 /// with the engine via [`Self::cache_metrics`] so engine-side bumps
2393 /// land in the same encoder output.
2394 pub fn new() -> Self {
2395 let decdn = Arc::new(DecdnMetrics::default());
2396 let inbound_streams = decdn.streams_active.get_or_create(&StreamLabels {
2397 direction: StreamDirection::Inbound,
2398 });
2399 let outbound_streams = decdn.streams_active.get_or_create(&StreamLabels {
2400 direction: StreamDirection::Outbound,
2401 });
2402 // Both directions of both outcome families export at zero from a fresh
2403 // registry, so a rate over them never starts absent.
2404 let inbound = StreamLabels {
2405 direction: StreamDirection::Inbound,
2406 };
2407 let outbound = StreamLabels {
2408 direction: StreamDirection::Outbound,
2409 };
2410 let inbound_completed = decdn.streams_completed.get_or_create(&inbound);
2411 let inbound_failed = decdn.streams_failed.get_or_create(&inbound);
2412 let outbound_completed = decdn.streams_completed.get_or_create(&outbound);
2413 let outbound_failed = decdn.streams_failed.get_or_create(&outbound);
2414 // Materialize every `reason` child up front so all three series export
2415 // at zero from a fresh registry (see the field docs on `Metrics`).
2416 // Driven off `ALL` and destructured positionally so a new variant
2417 // cannot compile without being materialized here — see `ALL`'s docs.
2418 let [
2419 probe_hold_exhausted,
2420 probe_hold_disabled,
2421 probe_hold_stake_lane_reserved,
2422 ] = ProbeHoldUnavailableReason::ALL.map(|reason| {
2423 decdn
2424 .probe_hold_unavailable
2425 .get_or_create(&ProbeHoldUnavailableLabels { reason })
2426 });
2427 // Materialize every RPC child so each `(method, outcome)` series and
2428 // each per-method histogram exports at zero from a fresh registry.
2429 let rpc = Arc::new(RpcMetrics::default());
2430 for method in RpcMethod::ALL {
2431 for outcome in RpcOutcome::ALL {
2432 rpc.rpc_requests
2433 .get_or_create(&RpcRequestLabels { method, outcome });
2434 }
2435 rpc.rpc_request_duration_seconds
2436 .get_or_create(&RpcMethodLabels { method });
2437 }
2438 let cache = Arc::new(CacheMetrics::default());
2439 let mut registry = Registry::default();
2440 registry.register(decdn.clone() as Arc<dyn MetricsGroup>);
2441 registry.register(rpc.clone() as Arc<dyn MetricsGroup>);
2442 // Cache metrics live under the `decdn_cache` prefix so they
2443 // share the `decdn_*` family the rest of the metrics use.
2444 registry
2445 .sub_registry_with_prefix("decdn")
2446 .register(cache.clone() as Arc<dyn MetricsGroup>);
2447 Self {
2448 registry: Arc::new(RwLock::new(registry)),
2449 decdn,
2450 rpc,
2451 cache,
2452 inbound_streams,
2453 outbound_streams,
2454 inbound_completed,
2455 inbound_failed,
2456 outbound_completed,
2457 outbound_failed,
2458 probe_hold_exhausted,
2459 probe_hold_disabled,
2460 probe_hold_stake_lane_reserved,
2461 published_regions: Mutex::new(BTreeSet::new()),
2462 started_at: Instant::now(),
2463 staker_set_watcher_down_since: Mutex::new(None),
2464 slash_watcher_down_since: Mutex::new(None),
2465 blacklist_watcher_down_since: Mutex::new(None),
2466 settlement_watcher_down_since: Mutex::new(None),
2467 fee_shares_watcher_down_since: Mutex::new(None),
2468 probe_holds: OnceLock::new(),
2469 }
2470 }
2471
2472 /// Attach the cache engine whose probe holds back
2473 /// `decdn_probe_hold_slots_used`. Each scrape samples
2474 /// [`CacheEngine::probe_hold_slots_used`], which sweeps expired holds, so
2475 /// the gauge falls back to `0` once holds lapse even with no probe
2476 /// traffic. Only the first attach takes effect.
2477 pub(crate) fn attach_probe_holds(&self, cache: CacheEngine) {
2478 // A second attach is a no-op: the runtime builds one cache per
2479 // process, so the first engine is the live one.
2480 let _ = self.probe_holds.set(cache);
2481 }
2482
2483 /// Shared `Arc<CacheMetrics>` for wiring into [`decdn_cache::CacheEngine`].
2484 pub fn cache_metrics(&self) -> Arc<CacheMetrics> {
2485 Arc::clone(&self.cache)
2486 }
2487
2488 /// Publish the count of inbound lanes the node holds open.
2489 ///
2490 /// Lane-scoped on purpose: the pool deposit behind those lanes is a
2491 /// pool-level on-chain quantity that no lane carries, so it is published
2492 /// separately by [`Self::set_pool_deposit_usdc`] on the redeemer's tick.
2493 /// A lane carries no pool deposit, so a lane-scoped caller could only ever
2494 /// pass zero for one (#2072).
2495 pub(crate) fn set_lanes_open(&self, open: usize) {
2496 self.decdn.lanes_open.set(sat(open));
2497 }
2498
2499 /// Publish the on-chain value still recoverable from the pools currently
2500 /// paying this node — `Σ (deposit − totalRedeemed)` over the distinct pools
2501 /// the redeemer planned lanes against.
2502 ///
2503 /// Called once per redeemer self-tick beside
2504 /// [`Self::set_unredeemed_usdc`], so the two money gauges share a cadence
2505 /// and can be read against each other: `unredeemed` is what the node has
2506 /// earned and not yet cashed, and this is the ceiling the pools can still
2507 /// pay it.
2508 pub(crate) fn set_pool_deposit_usdc(&self, remaining: U256) {
2509 self.decdn.pool_deposit_usdc.set(sat_u256(remaining));
2510 }
2511
2512 /// Publish the total raw USDC held in accepted-but-unredeemed vouchers.
2513 /// Called once per redeemer self-tick from the settlement sweep, so the
2514 /// gauge tracks what the node plans to redeem as of the last interval.
2515 pub(crate) fn set_unredeemed_usdc(&self, total: U256) {
2516 self.decdn.unredeemed_usdc.set(sat_u256(total));
2517 }
2518
2519 /// Publish the USDC balance of this node's own buyer wallet.
2520 pub(crate) fn set_buyer_wallet_usdc(&self, balance: U256) {
2521 self.decdn.buyer_wallet_usdc.set(sat_u256(balance));
2522 }
2523
2524 /// Count a refused pull whose lane watermark could not be established.
2525 pub(crate) fn buyer_lane_seed_failure(&self) {
2526 self.decdn.buyer_lane_seed_failures.inc();
2527 }
2528
2529 /// Count a bootstrap that could not determine whether this node already owns
2530 /// a payment pool.
2531 pub(crate) fn buyer_pool_adoption_failure(&self) {
2532 self.decdn.buyer_pool_adoption_failures.inc();
2533 }
2534
2535 /// Register iroh's transport metrics under the `decdn_iroh_` prefix so
2536 /// `socket_*`, `net_report_*`, etc. come out as
2537 /// `decdn_iroh_socket_*`, matching `adr/appendix-observability.md`'s naming convention.
2538 ///
2539 /// # Errors
2540 ///
2541 /// Returns an error if the registry lock is poisoned.
2542 pub fn register_iroh_endpoint(&self, ep: &Endpoint) -> anyhow::Result<()> {
2543 self.register_iroh_metrics(ep.metrics())
2544 }
2545
2546 /// Register an [`EndpointMetrics`] set under the `decdn_iroh_` prefix.
2547 ///
2548 /// Split out from [`Self::register_iroh_endpoint`] so
2549 /// `iroh_metrics_export_under_the_decdn_iroh_prefix` can register
2550 /// the same group from an `EndpointMetrics::default()` and pin the
2551 /// `decdn_iroh_*` names without standing up a socket. Without an endpoint
2552 /// those series are absent from `Metrics::new().encode()`.
2553 ///
2554 /// # Errors
2555 ///
2556 /// Returns an error if the registry lock is poisoned.
2557 fn register_iroh_metrics(&self, metrics: &EndpointMetrics) -> anyhow::Result<()> {
2558 let mut reg = self
2559 .registry
2560 .write()
2561 .map_err(|_| anyhow::anyhow!("metrics registry lock poisoned"))?;
2562 reg.sub_registry_with_prefix("decdn_iroh")
2563 .register_all(metrics);
2564 Ok(())
2565 }
2566
2567 /// Record a buyer `openChannel`-tx failure broken out by cause (#966): bumps
2568 /// the `decdn_pool_open_failures_{reason}_total` sibling counter for
2569 /// `reason`. Pairs with the structured `reason` field on the `warn!`/`debug!`
2570 /// in [`crate::node_origin`]. Distinct from
2571 /// [`Self::node_pull_pool_open_failure`], the unlabeled total (which also
2572 /// counts store/expired-reclaim causes that never reach the `openChannel`
2573 /// tx).
2574 pub fn pool_open_failure_by_reason(&self, reason: PoolOpenFailureReason) {
2575 match reason {
2576 PoolOpenFailureReason::InsufficientDeposit => {
2577 self.decdn.pool_open_failures_insufficient_deposit.inc();
2578 }
2579 PoolOpenFailureReason::ContractRevert => {
2580 self.decdn.pool_open_failures_contract_revert.inc();
2581 }
2582 PoolOpenFailureReason::RpcError => {
2583 self.decdn.pool_open_failures_rpc_error.inc();
2584 }
2585 }
2586 }
2587
2588 /// Record an ADR 041 serve-economics refusal: bumps the
2589 /// `decdn_serve_economics_refused_total` aggregate and the matching regime
2590 /// sibling. Enum-dispatch shape of [`Self::pool_open_failure_by_reason`];
2591 /// both counters are plain siblings on `self.decdn`, so they export at zero
2592 /// from a fresh registry without pre-materialization. Pairs with the
2593 /// structured `debug!` at the refusal site in [`crate::node_origin`].
2594 pub fn serve_economics_refused(&self, regime: ServeEconomicsRegime) {
2595 self.decdn.serve_economics_refused.inc();
2596 match regime {
2597 ServeEconomicsRegime::Warming => self.decdn.serve_economics_refused_warming.inc(),
2598 ServeEconomicsRegime::Amortized => self.decdn.serve_economics_refused_amortized.inc(),
2599 };
2600 }
2601
2602 /// Record a load-shed refusal broken out by cause into its sibling counter
2603 /// (`decdn_load_shed_refused_*_total`). Per `adr/appendix-observability.md`
2604 /// § Reason splits each shed reason is a sibling, not a label, because the
2605 /// remedies differ. The exhaustive `match` is the gate: a new
2606 /// [`crate::load_shed::ShedReason`] variant fails to compile until it is
2607 /// given a counter here. Pairs with the structured `debug!` at each shed
2608 /// site in [`crate::handlers::client`].
2609 pub fn load_shed_refused(&self, reason: crate::load_shed::ShedReason) {
2610 use crate::load_shed::ShedReason;
2611 match reason {
2612 ShedReason::NodeAtCapacity => self.decdn.load_shed_refused_node_at_capacity.inc(),
2613 ShedReason::EgressSaturated => self.decdn.load_shed_refused_egress_saturated.inc(),
2614 ShedReason::ClientAtCapacity => self.decdn.load_shed_refused_client_at_capacity.inc(),
2615 };
2616 }
2617
2618 /// Record a probe that could not be answered from a guaranteed eviction
2619 /// hold, broken out by cause on the `reason` label of
2620 /// `decdn_probe_hold_unavailable_total` (#1443). Hand-written rather than
2621 /// a `recorders!` entry because the pre-materialized child handles live on
2622 /// `Metrics`, whereas `recorders!` only reaches `self.decdn.$field`. Same
2623 /// enum-dispatch shape as [`Self::pool_open_failure_by_reason`], though
2624 /// that one's counters *are* siblings on `self.decdn`. Pairs with the
2625 /// structured `debug!` at each call site in [`crate::handlers::probe`].
2626 pub fn probe_hold_unavailable(&self, reason: ProbeHoldUnavailableReason) {
2627 match reason {
2628 ProbeHoldUnavailableReason::Exhausted => self.probe_hold_exhausted.inc(),
2629 ProbeHoldUnavailableReason::Disabled => self.probe_hold_disabled.inc(),
2630 ProbeHoldUnavailableReason::StakeLaneReserved => {
2631 self.probe_hold_stake_lane_reserved.inc()
2632 }
2633 };
2634 }
2635
2636 /// Record one JSON-RPC request on `decdn_rpc_requests_total{method,outcome}`
2637 /// and, when it resolved, its round trip on
2638 /// `decdn_rpc_request_duration_seconds{method}`. `elapsed` is `None` for a
2639 /// request the caller dropped. The `crate::rpc_metrics` transport layer is
2640 /// the one caller.
2641 pub(crate) fn rpc_request(
2642 &self,
2643 method: RpcMethod,
2644 outcome: RpcOutcome,
2645 elapsed: Option<Duration>,
2646 ) {
2647 self.rpc
2648 .rpc_requests
2649 .get_or_create(&RpcRequestLabels { method, outcome })
2650 .inc();
2651 if let Some(elapsed) = elapsed {
2652 self.rpc
2653 .rpc_request_duration_seconds
2654 .get_or_create(&RpcMethodLabels { method })
2655 .observe(elapsed.as_secs_f64());
2656 }
2657 }
2658
2659 /// Publish the active-staker set size per region: one
2660 /// `decdn_staker_set_active_by_region` child per entry in `counts`, plus
2661 /// `unknown` on `decdn_staker_set_active_unknown_region`. A region absent
2662 /// from `counts` has its child removed, so the map shows no stale country.
2663 /// Stale children are removed after the live ones are set, so a concurrent
2664 /// scrape never sees the family emptied part-way through an update. It can
2665 /// still see some regions at their new value and others at the old one.
2666 pub fn staker_set_active_by_region(&self, counts: &BTreeMap<Region, usize>, unknown: usize) {
2667 // Held for the whole update so two publishers cannot interleave their
2668 // set and remove passes. A poisoned lock still holds a whole set, since
2669 // it is only ever replaced in one assignment; at worst it misses a
2670 // child the panicking call created.
2671 let mut published = match self.published_regions.lock() {
2672 Ok(guard) => guard,
2673 Err(poisoned) => {
2674 tracing::warn!("published-regions Mutex poisoned; recovering inner state");
2675 self.published_regions.clear_poison();
2676 poisoned.into_inner()
2677 }
2678 };
2679 let family = &self.decdn.staker_set_active_by_region;
2680 for (region, count) in counts {
2681 family
2682 .get_or_create(&NodeRegionLabels {
2683 node_region: region.as_str().to_owned(),
2684 })
2685 .set(sat(*count));
2686 }
2687 for stale in published.iter().filter(|r| !counts.contains_key(r)) {
2688 family.remove(&NodeRegionLabels {
2689 node_region: stale.as_str().to_owned(),
2690 });
2691 }
2692 *published = counts.keys().copied().collect();
2693 self.decdn
2694 .staker_set_active_unknown_region
2695 .set(sat(unknown));
2696 }
2697
2698 /// Current value of the `dispatch_in_flight` gauge as a `u64`. Read
2699 /// by `admin_v1_health.in_flight_streams` (issue #604) so the
2700 /// `decdn node drain --wait` client can observe in-flight client
2701 /// streams reach 0 during a graceful drain.
2702 ///
2703 /// A negative gauge reading is forbidden by the `Permit`
2704 /// acquire/release RAII pair: `dispatch_permit_acquired` runs once
2705 /// before each `Permit` exists and `dispatch_permit_released` runs
2706 /// once on `Drop`. If the conversion fails the pair has been
2707 /// broken — we both `debug_assert!` (loud failure in tests/dev),
2708 /// log `tracing::error!` (visible in production), and return 0
2709 /// rather than wrapping to a huge `u64`. **Returning 0 here would
2710 /// be read by `decdn node drain --wait` as "drain complete" while
2711 /// real streams are still in flight**, so the loud log is the
2712 /// operator-actionable signal that something broke upstream.
2713 #[must_use]
2714 pub fn dispatch_in_flight_value(&self) -> u64 {
2715 let raw = self.decdn.dispatch_in_flight.get();
2716 if let Ok(v) = u64::try_from(raw) {
2717 return v;
2718 }
2719 debug_assert!(false, "dispatch_in_flight gauge went negative: {raw}");
2720 tracing::error!(
2721 raw,
2722 "dispatch_in_flight gauge is negative; the Permit \
2723 acquire/release pair is unbalanced. Reporting 0 to \
2724 avoid wraparound — `decdn node drain --wait` clients \
2725 may see a premature 'complete' signal until the \
2726 underlying accounting bug is fixed."
2727 );
2728 0
2729 }
2730
2731 /// Read the current value of the `rpc_healthy` gauge. Test-only —
2732 /// non-test callers should rely on the `OpenMetrics` endpoint rather
2733 /// than reaching into individual gauges.
2734 #[cfg(test)]
2735 pub(crate) fn rpc_healthy_value(&self) -> i64 {
2736 self.decdn.rpc_healthy.get()
2737 }
2738
2739 /// RAII guard that increments `active_connections` on construction and
2740 /// decrements it on drop, so the gauge stays correct even if the handler
2741 /// future is cancelled between open and close.
2742 pub fn connection_guard(&self) -> ConnectionGuard<'_> {
2743 ConnectionGuard::new(self)
2744 }
2745
2746 /// Count one inbound paid-delivery stream until the returned guard drops.
2747 pub(crate) fn inbound_stream_guard(&self) -> StreamGuard {
2748 StreamGuard::new(Arc::clone(&self.inbound_streams))
2749 }
2750
2751 /// Count one outbound paid-delivery stream until the returned guard drops.
2752 pub(crate) fn outbound_stream_guard(&self) -> StreamGuard {
2753 StreamGuard::new(Arc::clone(&self.outbound_streams))
2754 }
2755
2756 /// An inbound serve stream ended: `completed` when it delivered the whole
2757 /// request, otherwise failed (`decdn_streams_{completed,failed}_total`).
2758 pub(crate) fn inbound_stream_ended(&self, completed: bool) {
2759 if completed {
2760 self.inbound_completed.inc();
2761 } else {
2762 self.inbound_failed.inc();
2763 }
2764 }
2765
2766 /// An outbound node-to-node pull from one candidate ended: `completed` when
2767 /// it filled the blob, otherwise failed.
2768 pub(crate) fn outbound_stream_ended(&self, completed: bool) {
2769 if completed {
2770 self.outbound_completed.inc();
2771 } else {
2772 self.outbound_failed.inc();
2773 }
2774 }
2775
2776 /// Render the registry as `OpenMetrics` text — exactly the body the
2777 /// public `/metrics` HTTP endpoint serves. `pub` (not `pub(crate)`)
2778 /// so integration tests in sibling crates can assert on the exported
2779 /// series without scraping over TCP; it exposes no data the
2780 /// unauthenticated `/metrics` endpoint doesn't already.
2781 pub fn encode(&self) -> anyhow::Result<String> {
2782 let uptime = i64::try_from(self.started_at.elapsed().as_secs()).unwrap_or(i64::MAX);
2783 self.decdn.node_uptime_seconds.set(uptime);
2784
2785 // Recompute the per-watcher down-seconds gauges from their `down_since`,
2786 // mirroring `uptime_seconds` above (staker_set #783, slash #1032). See
2787 // `refresh_watcher_down_seconds` for the healthy-vs-outage and
2788 // poisoned-lock semantics.
2789 self.refresh_watcher_down_seconds();
2790
2791 if let Some(cache) = self.probe_holds.get() {
2792 self.decdn
2793 .probe_hold_slots_used
2794 .set(sat(cache.probe_hold_slots_used()));
2795 }
2796
2797 let reg = self
2798 .registry
2799 .read()
2800 .map_err(|_| anyhow::anyhow!("metrics registry lock poisoned"))?;
2801 reg.encode_openmetrics_to_string()
2802 .map_err(|e| anyhow::anyhow!("openmetrics encode failed: {e}"))
2803 }
2804}
2805
2806/// Generate the trivial forwarding recorders on [`Metrics`].
2807///
2808/// Each entry spells out both names — `method => field.op(value)` — because the
2809/// two diverge for many of them: the method reads as a singular event while the
2810/// field is plural (`probe_request` vs `probe_requests`), or the field is a
2811/// semantic rename of the action (`connection_opened` bumps
2812/// `active_connections`). Deriving one name from the
2813/// other would bind the wrong series, so neither is ever synthesized. (Field
2814/// names also carry their own encoder convention — most omit the `_total`
2815/// suffix the `OpenMetrics` encoder appends, though a few spell it out — but
2816/// that governs the exported name, not the method-to-field mapping here.)
2817///
2818/// Docs pass through as `$meta`, so multi-line rationale stays byte-identical
2819/// and keeps its call-site span for `clippy` and `rustdoc`.
2820///
2821/// What belongs here is a body that is a single `self.decdn.field.op(expr)` —
2822/// including a one-expression transform of the argument (`sat(n)`,
2823/// `i64::from(flag)`), which several entries do. Recorders whose body needs
2824/// more than that — branching, multiple statements, or touching `Metrics`'s own
2825/// `Mutex` state — do not fit this shape: the per-watcher downtime recorders
2826/// (which edge-trigger off a `Mutex<Option<Instant>>` `down_since` field) have
2827/// their own `watcher_downtime_recorders!` family below; anything else stays
2828/// hand-written in the `impl` block above. There is no
2829/// `Counter`/`Gauge` token to pick because the op is written explicitly per
2830/// entry; the one dangerous confusion, calling `dec()` on a `Counter`, does not
2831/// compile because `Counter` has no `dec()` (a wrong `set`/`inc` on the right
2832/// kind still would, so the entries are the source of truth).
2833///
2834/// Note that `rustfmt` does not format macro-invocation bodies, so the entry
2835/// table below is hand-maintained: keep it at one entry per line, within the
2836/// 100-column limit, and `cargo fmt` will neither help nor complain.
2837macro_rules! recorders {
2838 ($(
2839 $(#[$meta:meta])*
2840 $method:ident $(($arg:ident: $ty:ty))? => $field:ident . $op:ident ( $($val:expr)? ) ;
2841 )*) => {
2842 impl Metrics {
2843 $(
2844 $(#[$meta])*
2845 pub fn $method(&self $(, $arg: $ty)?) {
2846 self.decdn.$field.$op($($val)?);
2847 }
2848 )*
2849 }
2850 };
2851}
2852
2853/// Generate the per-watcher downtime recorders on [`Metrics`].
2854///
2855/// The chain watchers (`slash`, `staker_set`, `blacklist`, `settlement`) share
2856/// one downtime state machine: a `*_backoff_started` recorder that, on the
2857/// `None -> Some` edge into an error window, stamps the watcher's
2858/// `Mutex<Option<Instant>>` `down_since` field and bumps its `*_restarts`
2859/// counter exactly once per drift window; and a `*_cycle_established` recorder
2860/// that clears `down_since` on recovery. A poisoned lock skips the update; the
2861/// scrape-time recompute then reports the `*_down_seconds` gauge as `i64::MAX`
2862/// — the safe alerting direction, never `0` (which would mask an outage).
2863///
2864/// These cannot live in `recorders!` because their bodies branch and touch
2865/// `Metrics`'s own `Mutex` state rather than a single `self.decdn.field.op(v)`.
2866/// Each row spells out both method names (production hooks call them by exact
2867/// name, and this crate adds no `paste`) and labels the three backing field
2868/// idents (`down_since` on `Metrics`; `restarts`/`down_seconds` on `self.decdn`)
2869/// so positions aren't counted. Docs pass through as `$meta`, so each
2870/// recorder's rationale stays byte-identical and keeps its call-site span for
2871/// `clippy` and `rustdoc`.
2872///
2873/// Note that `rustfmt` does not format macro-invocation bodies, so the table
2874/// below is hand-maintained: keep the multi-line docs tidy and the field lines
2875/// within the 100-column limit — `cargo fmt` will neither help nor complain.
2876macro_rules! watcher_downtime_recorders {
2877 ($(
2878 $(#[$backoff_meta:meta])*
2879 $backoff:ident,
2880 $(#[$established_meta:meta])*
2881 $established:ident,
2882 down_since: $down_since:ident,
2883 restarts: $restarts:ident,
2884 down_seconds: $down_seconds:ident;
2885 )*) => {
2886 impl Metrics {
2887 $(
2888 $(#[$backoff_meta])*
2889 // Poison note: on a poisoned `down_since` lock the whole block is
2890 // skipped (anti-panic policy), so `*_restarts_total` is NOT
2891 // incremented and, because std poison is sticky, stays frozen for
2892 // the process's life. The paired `*_down_seconds` gauge covers this
2893 // — `refresh_watcher_down_seconds` reports `i64::MAX` on a poisoned
2894 // lock — so alert on the gauge for outages. The counter drives only
2895 // the flapping rule, which a frozen counter silences while the
2896 // stalled rule fires on the `i64::MAX` gauge
2897 // (appendix-observability § Watcher Liveness, #1322).
2898 pub fn $backoff(&self) {
2899 if let Ok(mut down_since) = self.$down_since.lock()
2900 && down_since.is_none()
2901 {
2902 *down_since = Some(Instant::now());
2903 self.decdn.$restarts.inc();
2904 }
2905 }
2906
2907 $(#[$established_meta])*
2908 pub fn $established(&self) {
2909 if let Ok(mut down_since) = self.$down_since.lock() {
2910 *down_since = None;
2911 }
2912 }
2913 )*
2914
2915 /// Recompute every watcher's `*_down_seconds` gauge from its
2916 /// `down_since` (true downtime), mirroring `uptime_seconds`. Called
2917 /// once per scrape from [`Self::encode`]. `None` (healthy cycle,
2918 /// including pre-bootstrap) reads `0` regardless of how long the
2919 /// cycle has been live; the value climbs once an error window opens
2920 /// and until the matching `*_cycle_established` clears `down_since`.
2921 /// A poisoned lock reports `i64::MAX` — the conservative, alerting
2922 /// direction for a downtime gauge, since reporting `0` would MASK an
2923 /// in-progress outage.
2924 fn refresh_watcher_down_seconds(&self) {
2925 $(
2926 self.decdn.$down_seconds.set(match self.$down_since.lock() {
2927 Ok(down_since) => down_since
2928 .map(|t| t.elapsed().as_secs())
2929 .map_or(0, |s| i64::try_from(s).unwrap_or(i64::MAX)),
2930 Err(_) => i64::MAX,
2931 });
2932 )*
2933 }
2934 }
2935 };
2936}
2937
2938recorders! {
2939 /// A `cdn/probe/v1` request was served.
2940 probe_request => probe_requests.inc();
2941
2942 /// Publish the configured `max_probe_holds` budget (registry-mandatory
2943 /// `decdn_probe_hold_slots_max`). Called once at runtime bring-up.
2944 probe_hold_slots_max(max: usize) => probe_hold_slots_max.set(sat(max));
2945
2946 /// An accepted voucher skipped one or more nonce values past
2947 /// `last_nonce + 1` (#747). Counted once per gapped voucher; the precise
2948 /// skip count rides the paired `tracing::warn!` in `apply_voucher`.
2949 voucher_nonce_gap => voucher_nonce_gaps.inc();
2950
2951 /// A redeem hint was dropped because the bounded advisory channel was full
2952 /// (`try_send` → `Full`, #751). Advisory, so a few drops are benign; a
2953 /// sustained rate means the redeemer is not keeping up with fan-out.
2954 redeem_hint_dropped => redeem_hints_dropped.inc();
2955
2956 /// A redeem hint was dropped because its lane is parked after a chain
2957 /// fault on the hint path (#2340). The next sweep retries the lane.
2958 redeem_hint_parked => redeem_hints_parked.inc();
2959
2960 /// A download-receipt audit write was dropped because the bounded writer
2961 /// queue was full (`try_send` → `Full`, #803). Audit-only, so a drop never
2962 /// affects settlement; a sustained rate means the writer is not keeping up
2963 /// with disk I/O and audit records are being lost.
2964 receipt_write_dropped => receipt_writes_dropped.inc();
2965
2966 /// An ADR 041 warming serve credit was dropped before it reached the ledger
2967 /// — the bounded aggregator queue was full, or the aggregator was gone. The
2968 /// drop is conservative (the source ledger stays lower than reality), but a
2969 /// sustained rate throttles speculative warming.
2970 warming_credit_dropped => warming_credits_dropped.inc();
2971
2972 /// An ADR 041 warming serve credit reached the ledger. Paired with
2973 /// `warming_credit_dropped` so an operator can tell "no credits dropped"
2974 /// from "no credits at all".
2975 warming_credit_applied => warming_credits_applied.inc();
2976
2977 /// A redemption step failed: a `redeemMany` chunk reverted or was refused
2978 /// at `send`, or a lane could not be planned or loaded. Pairs with the
2979 /// `warn!` at each site.
2980 redemption_failure => redemption_failures.inc();
2981
2982 /// A lane was held or dropped by `pool_is_redeemable` because its pool's
2983 /// chain-observed solvency ruled it out this pass (ADR 003).
2984 redemption_skipped_insolvent => redemption_skipped_insolvent.inc();
2985
2986 /// A lane with value owed was skipped this pass because its signer's
2987 /// capability has expired, or expires within the redeemer's landing slack
2988 /// (ADR 003 §Revocation).
2989 redemption_skipped_expired => redemption_skipped_expired.inc();
2990
2991 /// `n` lanes were held or dropped by `pool_is_redeemable` in one planning
2992 /// pass (ADR 003); the batched form of `redemption_skipped_insolvent`.
2993 redemption_skipped_insolvent_by(n: u64) => redemption_skipped_insolvent.inc_by(n);
2994
2995 /// `n` lanes were dropped from a redeem batch by the pre-submit on-chain
2996 /// watermark reconciliation because the chain already shows them settled to
2997 /// their claim value (the `redeemMany` no-op guard, avoided before it costs
2998 /// gas).
2999 redemption_reconciled_skip_by(n: u64) => redemption_reconciled_skip.inc_by(n);
3000
3001 /// A pre-submit on-chain watermark batch landed and was reconciled against
3002 /// the chain — the attempt signal that makes a zero
3003 /// `redemption_reconciled_skip` readable.
3004 redemption_reconcile_ok => redemption_reconcile_ok.inc();
3005
3006 /// A pre-submit on-chain watermark batch did not land (RPC error, timeout,
3007 /// or a mismatched return length). Fail-open: the lanes it could not read
3008 /// are submitted unchanged.
3009 redemption_reconcile_failure => redemption_reconcile_failures.inc();
3010
3011 /// A buyer-side reclaim-sweep attempt (`try_reclaim`) failed — an RPC/receipt
3012 /// error, an on-chain revert, or a failed store write when clearing the local
3013 /// record (#906). Pairs with the per-attempt `warn!` in `try_reclaim` and the
3014 /// threshold `error!` in `reclaim_once`.
3015 buyer_reclaim_failure => buyer_reclaim_failures.inc();
3016
3017 /// Buyer-pool rows skipped as undecodable during one successful store
3018 /// hydration (#1271). Counted once per skipped row per load attempt (each of
3019 /// the startup, reconcile, and reclaim loads bumps it), so a persistent
3020 /// malformed row keeps the escrowed-but-untracked alert active. The
3021 /// `usize` count is saturated into the `u64` counter.
3022 buyer_pool_store_skipped_undecodable_records(count: usize)
3023 => buyer_pool_store_skipped_undecodable_records.inc_by(u64::try_from(count).unwrap_or(u64::MAX));
3024
3025 /// A background low-water top-up (#1146) landed: a reused buyer channel below
3026 /// its 20% low-water mark was re-funded to the working deposit. Pairs with the
3027 /// `info!` in `spawn_refill_if_low`.
3028 buyer_topup_ok => buyer_topup_ok.inc();
3029
3030 /// A background low-water top-up (#1146) did not cleanly land — the allowance
3031 /// re-approval or the `topUp` submit/receipt errored or reverted, OR the `topUp`
3032 /// landed on-chain but the local row vanished/rotated during the RPC
3033 /// (`DepositOutcome::UnknownProvider` / `ChannelMismatch`, i.e. escrowed-but-
3034 /// untracked). Folding the untracked case in here keeps stranded deposits
3035 /// visible on this counter. Best-effort, so the channel is left un-topped; pairs
3036 /// with the `warn!` (or `top_up`'s own error!/warn!) around the call in
3037 /// `spawn_refill_if_low`.
3038 buyer_topup_failure => buyer_topup_failure.inc();
3039
3040 /// The settlement watcher failed to forget a reclaimed pool's lane state
3041 /// (`forget_lane`) on a `PoolReclaimed` event, in `PoolSettlementSink`
3042 /// (`payment_settlement.rs`). The failure is swallowed — logged with a
3043 /// per-site `warn!` ("failed to forget reclaimed lane") and the cursor
3044 /// advances — so it is a lane-store health signal, not a stuck settlement.
3045 watcher_persist_failure => watcher_persist_failures.inc();
3046
3047 /// A lane-store background flush failed — the dirty in-memory set is
3048 /// retained and retried on the next timer tick, or a final flush on
3049 /// shutdown failed. Pairs with the `warn!`s in the background flush task
3050 /// and the shutdown flush in `runtime/mod.rs`.
3051 lane_flush_failure => lane_flush_failures.inc();
3052
3053 /// A distinct slash against this node's operator was detected by the slash
3054 /// watcher (#1032). Counts each `slashId` once (backfill + live dedup).
3055 slash_detected => slashes_detected.inc();
3056 /// A slash-watcher resync failed and the current detected-slash set was
3057 /// kept. Bumps `slash_resync_failures_total`.
3058 slash_resync_failure => slash_resync_failures.inc();
3059
3060 /// A `nodeIdOf(operator)` resolution for an operator-indexed event failed,
3061 /// dropping the membership change (#788, [`crate::dht::chain_staker_set`]).
3062 /// Bumps `staker_set_watcher_resolve_failures_total`. Pairs with the
3063 /// per-failure `warn!` in [`crate::dht::capacity_bond_registry`]'s
3064 /// `RegistrySink::on_operator_change`.
3065 staker_set_watcher_resolve_failure => staker_set_watcher_resolve_failures.inc();
3066
3067 /// Publish the current cached active-staker set size (#783). Sampled on
3068 /// every membership change the watcher applies, so the gauge tracks the
3069 /// cached view — which under a watcher outage is exactly the (possibly
3070 /// stale) set that admission decisions read.
3071 staker_set_active_count(count: usize) => staker_set_active_count.set(sat(count));
3072
3073 /// Publish the current cached `NodeId → operator address` binding count
3074 /// (#831). Sampled on every binding change the node-address watcher applies,
3075 /// so it tracks the cached view the pull path resolves against.
3076 node_address_directory_size(count: usize) => node_address_directory_size.set(sat(count));
3077
3078 /// A node-to-node pull orchestration found ≥1 candidate and is attempting a
3079 /// fill (#831).
3080 node_pull_attempt => node_pull_attempts.inc();
3081
3082 /// A node-to-node pull delivered verified bytes (#831).
3083 node_pull_success => node_pull_success.inc();
3084
3085 /// A cache miss surfaced no provider from discovery (#831).
3086 node_pull_no_providers => node_pull_no_providers.inc();
3087
3088 /// A cache-miss pull was served from the ADR 001 probe cache: no DHT lookup,
3089 /// no probe fanout (#1165).
3090 probe_cache_hit => probe_cache_hits.inc();
3091
3092 /// A cache-miss pull found no usable probe-cache entry and ran a fresh
3093 /// lookup + probe (#1165).
3094 probe_cache_miss => probe_cache_misses.inc();
3095
3096 /// One probe round's collection window ended (ADR 001 §Probe response
3097 /// collection): `elapsed` runs from the start of the concurrent probes to
3098 /// the end of collection.
3099 probe_collection_latency(elapsed: Duration) =>
3100 probe_collection_latency_seconds.observe(elapsed.as_secs_f64());
3101
3102 /// One node-to-node pull leg read its first bao bytes `elapsed` after its
3103 /// open started.
3104 node_pull_first_byte(elapsed: Duration) =>
3105 node_pull_first_byte_seconds.observe(elapsed.as_secs_f64());
3106
3107 /// A DHT lookup was truncated at `MAX_LOOKUP_ROUNDS` while still finding closer nodes
3108 /// (#1145 review). A sustained rate means the ceiling is too low for the network size.
3109 dht_lookup_round_ceiling => dht_lookup_round_ceiling.inc();
3110
3111 /// An upstream served hash-mismatched bytes for a paid pull (#831).
3112 node_pull_corruption => node_pull_corruption.inc();
3113
3114 /// A probe or pull to a candidate failed at the transport (#831).
3115 node_pull_unreachable => node_pull_unreachable.inc();
3116
3117 /// A probed peer claimed this node's own region but exceeded the ADR 030
3118 /// latency ceiling, so it took the local reputation penalty (#1177).
3119 node_region_latency_penalty => node_region_latency_penalty.inc();
3120
3121 /// A buyer channel open/reuse failed before a pull could start (#831).
3122 node_pull_pool_open_failure => node_pull_pool_open_failures.inc();
3123
3124 /// A selected upstream claimed a `total_bytes` above this node's
3125 /// `max_blob_size` ceiling and the buyer rejected it before buffering
3126 /// (#840). A buyer-side policy decision, so it does not score the provider.
3127 node_pull_too_large => node_pull_too_large.inc();
3128
3129 /// A selected upstream quoted a per-MB rate above this node's effective buyer
3130 /// ceiling and the buyer refused before paying (#1375). A buyer-side policy
3131 /// decision, so it does not score the provider.
3132 node_pull_rate_above_ceiling => node_pull_rate_above_ceiling.inc();
3133
3134 /// Record a paid-delivery (`serve_stream`) request refused because the
3135 /// blob was evicted between probe and stream (#876).
3136 serve_stream_rejected_evicted_since_probe => serve_stream_rejected_evicted_since_probe.inc();
3137
3138 /// Record a `serve_stream` request refused on a cache miss that
3139 /// pull-through could not fill (#876).
3140 serve_stream_rejected_cache_miss => serve_stream_rejected_cache_miss.inc();
3141
3142 /// Record a `serve_stream` request refused by a local store fault,
3143 /// signed as `Declined` (#876).
3144 serve_stream_rejected_internal_error => serve_stream_rejected_internal_error.inc();
3145
3146 /// Record a `serve_stream` request refused on an unknown lane (#876).
3147 serve_stream_rejected_unknown_lane => serve_stream_rejected_unknown_lane.inc();
3148
3149 /// Record a `serve_stream` request refused because the client binding did
3150 /// not authorize the named channel (#876).
3151 serve_stream_rejected_owner_mismatch => serve_stream_rejected_owner_mismatch.inc();
3152 /// A client binding failed to verify and the stream was reset.
3153 serve_stream_rejected_bad_binding => serve_stream_rejected_bad_binding.inc();
3154 /// A probe request could not be read.
3155 probe_read_fault => probe_read_faults.inc();
3156
3157 /// Record a `serve_stream` cache-miss refused by the pre-flight deposit guard
3158 /// (#856): the requesting channel could not cover the worst-case blob cost,
3159 /// so no upstream pull was started.
3160 serve_stream_rejected_insufficient_deposit => serve_stream_rejected_insufficient_deposit.inc();
3161
3162 /// Record a `serve_stream` admission refused because a wired `PoolView` could
3163 /// not confirm the pool on-chain (absent, closed, or the admit `getPool`
3164 /// faulted), or a `getAuthorization` fault left the signer with no cached
3165 /// registered read — kept distinct from a real deposit-exhaustion refusal.
3166 serve_stream_rejected_pool_unconfirmed => serve_stream_rejected_pool_unconfirmed.inc();
3167
3168 /// Record a `serve_stream` admission refused because its pool is `Closing`.
3169 serve_stream_rejected_pool_closing => serve_stream_rejected_pool_closing.inc();
3170
3171 /// Record an admit signer confirm the signer-auth cache answered.
3172 serve_signer_auth_cached => serve_signer_auth_cached.inc();
3173
3174 /// Record an admit signer confirm for a `(pool, signer)` with no held read.
3175 serve_signer_auth_first_read => serve_signer_auth_first_read.inc();
3176
3177 /// Record an admit signer confirm that re-reads an `Unregistered` signer.
3178 serve_signer_auth_reread => serve_signer_auth_reread.inc();
3179
3180 /// Record a `serve_stream` admission refused because the request's voucher
3181 /// signer has drained its shared on-chain `cap` (`cap − spent` below the serve
3182 /// floor) or its registration has expired — a capability this node could never
3183 /// cash.
3184 serve_stream_rejected_signer_cap_exhausted => serve_stream_rejected_signer_cap_exhausted.inc();
3185
3186 /// Record a live serve stopped mid-stream because its voucher signer drained
3187 /// its shared on-chain `cap` at other nodes after admission, so the headroom the
3188 /// node holds on the lane no longer covers a serve floor.
3189 serve_stream_midstream_signer_cap_exhausted => serve_stream_midstream_signer_cap_exhausted.inc();
3190
3191 /// Record a `serve_stream` delivery refused because this capability signer hit its
3192 /// per-signer live concurrency cap of un-vouchered reservation, while the pool as a
3193 /// whole can still pay.
3194 serve_stream_rejected_signer_floor_at_cap => serve_stream_rejected_signer_floor_at_cap.inc();
3195
3196 /// Record a whole-blob request from an active staker refused while this
3197 /// node's own upstream open for the blob was in progress (#2224).
3198 serve_stream_rejected_pull_loop_guard => serve_stream_rejected_pull_loop_guard.inc();
3199
3200 /// Record a `serve_stream` delivery refused because the requested bounded
3201 /// range is out of bounds for the blob (ADR 005 §Bounded byte ranges).
3202 serve_stream_rejected_range_not_satisfiable
3203 => serve_stream_rejected_range_not_satisfiable.inc();
3204
3205 /// Record a `serve_stream` pull-through refused because the requested range
3206 /// starts at or past the node's `max_blob_size` ceiling (ADR 005
3207 /// §`max_blob_size` enforcement).
3208 serve_stream_rejected_blob_too_large => serve_stream_rejected_blob_too_large.inc();
3209
3210 /// Record a `serve_stream` delivery refused because the blob is on the
3211 /// operator's local denylist (ADR 011 §Local Denylist).
3212 serve_stream_rejected_hash_denied => serve_stream_rejected_hash_denied.inc();
3213
3214 /// Record a `serve_stream` delivery refused because the blob is on the
3215 /// governance blacklist (ADR 011 §On Blacklist Event).
3216 serve_stream_rejected_chain_hash_denied => serve_stream_rejected_chain_hash_denied.inc();
3217
3218 /// Record a `serve_stream` delivery refused because the channel's funding
3219 /// address is a blacklisted origin (ADR 011 §On Blacklist Event).
3220 serve_stream_rejected_origin_denied => serve_stream_rejected_origin_denied.inc();
3221
3222 /// Record a `serve_stream` delivery declined by the origin-only policy
3223 /// (#1759, #1766): a live probe of this node's own backend genuinely does
3224 /// not hold the hash.
3225 serve_stream_rejected_foreign_declined => serve_stream_rejected_foreign_declined.inc();
3226
3227 /// Record a `serve_stream` request refused because the node's chain reads
3228 /// are stale past `blockchain.chain_staleness_grace_sec` (ADR 011 § Serving
3229 /// while chain-stale).
3230 serve_stream_rejected_chain_stale => serve_stream_rejected_chain_stale.inc();
3231
3232 /// Record an in-flight delivery cut off at an MB boundary because a takedown
3233 /// landed after the stream opened (ADR 011 §On Blacklist Event).
3234 serve_stream_terminated_takedown => serve_stream_terminated_takedown.inc();
3235
3236 /// Record a `serve_stream` cache-hit request refused by the load-shed policy
3237 /// (egress saturation).
3238 serve_stream_rejected_load_shed_hit => serve_stream_rejected_load_shed_hit.inc();
3239
3240 /// Record a `serve_stream` cache-miss request refused by the load-shed policy
3241 /// (concurrency pressure or per-client fairness).
3242 serve_stream_rejected_load_shed_miss => serve_stream_rejected_load_shed_miss.inc();
3243
3244 /// Record a serve leg refusing to cut or frame a `ChunkData` because its own
3245 /// byte accounting did not add up. Node-side bug, never peer behaviour.
3246 serve_frame_accounting_fault => serve_frame_accounting_fault.inc();
3247
3248 /// Record a `cdn/client/v1` delivery that ended on a node-side fault rather than
3249 /// on a peer hang-up or a client payment fault. Node-side bug, never peer
3250 /// behaviour.
3251 serve_stream_node_fault => serve_stream_node_fault.inc();
3252
3253 /// Record a `cdn/client/v1` delivery that ended because the payer spent its
3254 /// per-chunk proof budget without settling the chunk. The payer's fault, not a
3255 /// node fault.
3256 serve_stream_proof_budget_exhausted => serve_stream_proof_budget_exhausted.inc();
3257
3258 /// Record a `cdn/client/v1` stream reset because the connection's stream cap
3259 /// was full.
3260 serve_stream_rejected_stream_cap_full => serve_stream_rejected_stream_cap_full.inc();
3261
3262 /// Record a `cdn/client/v1` stream reset because its first request could not
3263 /// be read. Peer-side.
3264 serve_stream_request_unreadable => serve_stream_request_unreadable.inc();
3265
3266 /// Record a paying `cdn/client/v1` stream stopped on a rejected voucher.
3267 serve_stream_voucher_rejected => serve_stream_voucher_rejected.inc();
3268
3269 /// Record a `cdn/client/v1` stream the requester left before any voucher
3270 /// credited a byte. Peer-side.
3271 serve_stream_client_declined => serve_stream_client_declined.inc();
3272
3273 /// Record a paying `cdn/client/v1` stream the requester left after a voucher
3274 /// credited bytes. Peer-side.
3275 serve_stream_client_abandoned => serve_stream_client_abandoned.inc();
3276
3277 /// Record the current measured egress EWMA, bytes/sec.
3278 load_shed_egress_bps(bps: i64) => load_shed_egress_bps.set(bps);
3279
3280 /// Record whether the load-shed policy considers the node pressured (1 for yes, 0 for no).
3281 load_shed_pressure_active(active: bool) => load_shed_pressure_active.set(i64::from(active));
3282
3283 /// Publish node-wide serves in flight, sampled from the shed controller
3284 /// beside the egress EWMA. `u32 -> i64` is lossless.
3285 load_shed_streams_in_flight(n: u32) => load_shed_streams_in_flight.set(i64::from(n));
3286
3287 /// Record an above-floor warming buy downgraded to the amortized floor
3288 /// because the source's per-source allowance was spent (ADR 041).
3289 warming_speculative_blocked => warming_speculative_blocked.inc();
3290
3291 /// Publish how many upstream sources currently hold no warming allowance
3292 /// (ADR 041), sampled beside the egress EWMA.
3293 warming_sources_blocked(n: usize) => warming_sources_blocked.set(sat(n));
3294
3295 /// The window-paced serve loop paused the upstream pull at the ramped credit
3296 /// window to wait for the downstream voucher to clear (#856, #1669).
3297 node_pull_through_window_paused => node_pull_through_window_paused.inc();
3298
3299 /// The window-paced serve loop paused the upstream pull until the ramped
3300 /// window opened to the minimum draw (#2061).
3301 node_pull_through_min_draw_waits => node_pull_through_min_draw_waits.inc();
3302
3303 /// A window-paced pause of the upstream pull ended after `elapsed`.
3304 node_pull_through_wait(elapsed: Duration) =>
3305 node_pull_through_wait_seconds.observe(elapsed.as_secs_f64());
3306
3307 /// A window-paced serve was abandoned because the requesting client dropped
3308 /// or underpaid mid-pull (#856).
3309 node_pull_through_client_abandoned => node_pull_through_client_abandoned.inc();
3310
3311 /// A serve-miss pull ingested upstream bytes that failed bao verification
3312 /// against the content root (#856/#915).
3313 node_pull_through_upstream_verify_failed => node_pull_through_upstream_verify_failed.inc();
3314
3315 /// The node entered `serve_via_backend_origin` — the stream-while-store
3316 /// serve tier fired for this request (#1130). Counted at entry, before any
3317 /// admission guard; distinguishes this tier from the buffered
3318 /// `populate_local` fallback regardless of this request's eventual outcome.
3319 local_outboard_serve => local_outboard_serves.inc();
3320
3321 /// The blob-availability gate classified a paid serve as servable from a
3322 /// complete local blob. Bumped before the shed gate and before any fill
3323 /// tier can run.
3324 serve_cache_hit => serve_cache_hit.inc();
3325 /// The gate classified a paid serve as servable from a partial blob whose
3326 /// held chunk groups cover the requested span (#1506).
3327 serve_cache_partial_hit => serve_cache_partial_hit.inc();
3328 /// The gate could not satisfy a paid serve locally, so a fill tier runs.
3329 serve_cache_miss => serve_cache_miss.inc();
3330
3331 /// A buyer→upstream pull hit this node's own `pull_timeout` deadline (#857).
3332 /// A buyer-side condition, so it does not score the provider's reputation.
3333 node_pull_timeout => node_pull_timeout.inc();
3334
3335 /// An upstream rejected a voucher this node presented mid-pull (#857) — a
3336 /// buyer payment-side fault, so it does not score the provider's reputation.
3337 node_pull_voucher_rejected => node_pull_voucher_rejected.inc();
3338
3339 /// A lane can no longer pay but the pool's deposit is still escrowed, so the row was
3340 /// KEPT for the reclaim sweep and the provider suppressed instead (#1145 review).
3341 /// Money at rest — see the counter's docs.
3342 node_pull_pool_wedged => node_pull_pool_wedged.inc();
3343
3344 /// A selected upstream refused delivery up front (#1144). Counts every wire
3345 /// class; none scores the provider's reputation.
3346 node_pull_refused => node_pull_refused.inc();
3347
3348 /// A refusal this node cannot attribute to the upstream (#1520) — `NotFound`
3349 /// or `Unfunded`. A rate approaching `node_pull_refused` means nobody will
3350 /// serve us, which is usually our own deposit or binding, not their fault.
3351 node_pull_refused_unattributable => node_pull_refused_unattributable.inc();
3352
3353 /// An upstream shed a probe or pull at the transport with `APP_ERR_RATE_LIMITED`
3354 /// (#1986). Suppressed briefly, never scored — see the counter's docs.
3355 node_upstream_rate_limited => node_upstream_rate_limited.inc();
3356
3357 /// A ranged run completed one wait on an upstream's backpressure refusal
3358 /// instead of dropping the sole covering source (#2178) — see the counter's
3359 /// docs.
3360 node_pull_backpressure_backoffs => node_pull_backpressure_backoffs.inc();
3361
3362 /// The sole covering source outlasted the backpressure wait budget and the
3363 /// assembly ended short (#2178) — see the counter's docs.
3364 node_pull_backpressure_exhausted => node_pull_backpressure_exhausted.inc();
3365
3366 /// A clean upstream leg moved neither frontier and the run dropped its source
3367 /// (#2194) — see the counter's docs.
3368 node_pull_leg_no_progress => node_pull_leg_no_progress.inc();
3369
3370 /// An upstream went silent mid-stream (#1134); the pull was abandoned and the
3371 /// provider scored `Unreachable`.
3372 node_pull_stalled => node_pull_stalled.inc();
3373
3374 /// A pull failed for a LOCAL reason (#1145 review) — signer, encode, range, or
3375 /// this node's own cache store — so the upstream was exonerated. Says nothing
3376 /// about the network; any sustained rate means this node cannot complete a pull.
3377 node_pull_local_fault => node_pull_local_fault.inc();
3378
3379 /// A buyer channel open outlived the per-candidate budget (#1143). The open
3380 /// continues in the background; the pull moves on. No reputation effect.
3381 node_pull_pool_open_pending => node_pull_pool_open_pending.inc();
3382
3383 /// A pull paid ≥1 voucher but persisting the buyer channel resume watermark
3384 /// failed (#852); the channel's stored progress now lags the upstream.
3385 node_pull_progress_persist_failure => node_pull_progress_persist_failures.inc();
3386
3387 /// A paid voucher's watermark was DROPPED because the provider's channel slot had
3388 /// been replaced by a newer open before the write landed (#1145 review).
3389 node_pull_progress_dropped => node_pull_progress_dropped.inc();
3390
3391 /// A concurrent settle on the shared channel ledger persisted a higher watermark first, so
3392 /// this write was superseded (benign under `BuyerLedgers`; #1145 review).
3393 node_pull_progress_superseded => node_pull_progress_superseded.inc();
3394
3395 /// A miss fill's funding recovery step funded the next pass (#1530).
3396 node_pull_recovery_step => node_pull_recovery_step.inc();
3397
3398 /// A miss fill's funding recovery step had no way to fund the next pass,
3399 /// or its funding tx failed (#1530).
3400 node_pull_recovery_step_refused => node_pull_recovery_step_refused.inc();
3401
3402 /// The delivery handler abandoned a pull-through at its deadline (#831).
3403 node_pull_through_timeout => node_pull_through_timeouts.inc();
3404
3405 /// A cache-engine error (not a clean miss) was hit filling a miss (#831).
3406 node_pull_through_error => node_pull_through_errors.inc();
3407
3408 /// A `getOrigins` lookup failed on a cold-namespace cache miss in the lazy
3409 /// origin directory. The lookup fails closed (resolves no origins for
3410 /// that request) and the failure is not cached, so this is the drift
3411 /// signal to alert on.
3412 origin_directory_get_origins_failure => origin_directory_get_origins_failures.inc();
3413
3414 /// Publish the current namespace count held in the lazy origin directory
3415 /// cache (positive + negative entries), after an insert.
3416 origin_directory_cache_size(count: usize)
3417 => origin_directory_cache_size.set(sat(count));
3418 /// A connection was accepted and handed to a handler.
3419 connection_opened => active_connections.inc();
3420 /// A connection closed. Pairs with `connection_opened`.
3421 connection_closed => active_connections.dec();
3422
3423 /// A `cdn/client/v1` connection was reaped by the application-layer idle
3424 /// closer (ADR 005 §Connection lifetime).
3425 client_idle_close => client_idle_close.inc();
3426
3427 /// An OTLP span-export batch failed (`commands::otlp`'s counting exporter).
3428 otlp_export_failure => otlp_export_failures.inc();
3429
3430 /// Set the RPC health gauge. `true` -> 1 (reachable), `false` -> 0
3431 /// (unreachable). Driven by the watchdog task spawned in
3432 /// `runtime::run`.
3433 rpc_healthy(ok: bool) => rpc_healthy.set(i64::from(ok));
3434
3435 /// Record a connection rejected by the global concurrency semaphore.
3436 dispatch_rejected_global => dispatch_rejected_global.inc();
3437
3438 /// Record a connection rejected by the per-source rate limiter.
3439 dispatch_rejected_per_source => dispatch_rejected_per_source.inc();
3440
3441 /// Increment the in-flight dispatch permit gauge.
3442 dispatch_permit_acquired => dispatch_in_flight.inc();
3443
3444 /// Decrement the in-flight dispatch permit gauge.
3445 dispatch_permit_released => dispatch_in_flight.dec();
3446
3447 /// Record a relay-only connection accepted while the per-source
3448 /// layer was enabled but no peer IP could be resolved at accept
3449 /// time.
3450 dispatch_per_source_skipped_no_addr => dispatch_per_source_skipped_no_addr.inc();
3451
3452 /// Record an inbound connection that arrived with a direct IP path.
3453 inbound_connection_direct => inbound_connections_direct.inc();
3454
3455 /// Record an inbound connection that arrived over the relay only.
3456 inbound_connection_relayed => inbound_connections_relayed.inc();
3457
3458 /// Set whether the host has a public address on its default route.
3459 node_public_address(public: bool) => node_public_address.set(i64::from(public));
3460
3461 /// Record a `cdn/dht/v1` request rejected at the per-peer layer.
3462 dht_rate_limit_rejected_per_peer => dht_rate_limit_rejected_per_peer.inc();
3463
3464 /// Record a `cdn/dht/v1` request rejected at the per-IP layer.
3465 dht_rate_limit_rejected_per_ip => dht_rate_limit_rejected_per_ip.inc();
3466
3467 /// Record a `cdn/dht/v1` request rejected at the global layer.
3468 dht_rate_limit_rejected_global => dht_rate_limit_rejected_global.inc();
3469
3470 /// Record a `retain_recent` sweep of the per-IP DHT keyed-limiter
3471 /// map (#645).
3472 dht_rate_limit_prune_sweep_per_ip => dht_rate_limit_prune_sweeps_per_ip.inc();
3473
3474 /// Record a `retain_recent` sweep of the per-peer DHT keyed-limiter
3475 /// map (#645).
3476 dht_rate_limit_prune_sweep_per_peer => dht_rate_limit_prune_sweeps_per_peer.inc();
3477
3478 /// Set the per-IP DHT keyed-limiter tracked-size gauge (#645). The
3479 /// `try_from(...).unwrap_or(i64::MAX)` clamp matches the existing
3480 /// gauge-set pattern elsewhere in this module and stays within the
3481 /// workspace's anti-panic policy.
3482 dht_rate_limit_tracked_per_ip_set(n: usize) => dht_rate_limit_tracked_per_ip.set(sat(n));
3483
3484 /// Set the per-peer DHT keyed-limiter tracked-size gauge (#645).
3485 dht_rate_limit_tracked_per_peer_set(n: usize) => dht_rate_limit_tracked_per_peer.set(sat(n));
3486
3487 /// Record a `cdn/probe/v1` request rejected at the per-peer layer.
3488 probe_rate_limit_rejected_per_peer => probe_rate_limit_rejected_per_peer.inc();
3489
3490 /// Record a `cdn/probe/v1` request rejected at the per-IP layer.
3491 probe_rate_limit_rejected_per_ip => probe_rate_limit_rejected_per_ip.inc();
3492
3493 /// Record a `cdn/probe/v1` request rejected at the global layer.
3494 probe_rate_limit_rejected_global => probe_rate_limit_rejected_global.inc();
3495
3496 /// Record a `retain_recent` sweep of the per-IP probe keyed-limiter
3497 /// map (#645).
3498 probe_rate_limit_prune_sweep_per_ip => probe_rate_limit_prune_sweeps_per_ip.inc();
3499
3500 /// Record a `retain_recent` sweep of the per-peer probe keyed-limiter
3501 /// map (#645).
3502 probe_rate_limit_prune_sweep_per_peer => probe_rate_limit_prune_sweeps_per_peer.inc();
3503
3504 /// Set the per-IP probe keyed-limiter tracked-size gauge (#645).
3505 probe_rate_limit_tracked_per_ip_set(n: usize) => probe_rate_limit_tracked_per_ip.set(sat(n));
3506
3507 /// Set the per-peer probe keyed-limiter tracked-size gauge (#645).
3508 probe_rate_limit_tracked_per_peer_set(n: usize)
3509 => probe_rate_limit_tracked_per_peer.set(sat(n));
3510
3511 /// Record a `cdn/dht/v1` request that was admitted by the rate
3512 /// limiter but failed after that (frame decode, write, encode,
3513 /// timeout, etc).
3514 dht_request_failed => dht_requests_failed.inc();
3515
3516 /// Record a `Store` rejected by the `holder != authenticated NodeId`
3517 /// check (ADR 022 §STORE Flow line 140 — lying-holder attack).
3518 dht_store_rejected_holder_mismatch => dht_store_rejected_holder_mismatch.inc();
3519
3520 /// Record a `Store` rejected by the active-staker filter (ADR 022
3521 /// §STORE Flow line 140 — non-staked publisher).
3522 dht_store_rejected_non_staked => dht_store_rejected_non_staked.inc();
3523
3524 /// Record a `Store` rejected by the per-publisher quota (ADR 022
3525 /// §Content Records and TTL — 200-record hard cap).
3526 dht_store_rejected_quota => dht_store_rejected_quota.inc();
3527
3528 /// Record a `Store` admitted to the record store (newly inserted or
3529 /// refreshed).
3530 dht_store_accepted => dht_store_accepted.inc();
3531
3532 /// Record a `BatchStore` that reached per-hash admission (passed
3533 /// stage-1 rate limiting + the batch-level holder check, #648).
3534 dht_batch_store_received => dht_batch_store_received.inc();
3535
3536 /// Record `count` `BatchStore` hashes deferred (acked `false`)
3537 /// because the two-stage rate-limit budget ran out before reaching
3538 /// them (ADR 022 §Batch token accounting, #648).
3539 dht_batch_store_hashes_deferred_rate_limit(count: u64)
3540 => dht_batch_store_hashes_deferred_rate_limit.inc_by(count);
3541
3542 /// Record a republish lag sweep starting (the cache-commit broadcast
3543 /// channel reported `Lagged`).
3544 dht_republish_lag_sweep => dht_republish_lag_sweeps.inc();
3545 /// Record a lag folded into an already-running or already-queued sweep
3546 /// rather than starting a walk of its own.
3547 dht_republish_lag_sweep_coalesced => dht_republish_lag_sweeps_coalesced.inc();
3548 /// Record a lag-sweep walk that died in a panic instead of completing.
3549 dht_republish_lag_sweep_panicked => dht_republish_lag_sweep_panics.inc();
3550 /// Record `count` hashes a lag sweep newly scheduled for republish.
3551 dht_republish_sweep_reseeded(count: u64) => dht_republish_sweep_reseeded.inc_by(count);
3552 /// Record a bulk republish seed (boot cold start or lag sweep) that could
3553 /// not walk the store and covered only the origin-held half.
3554 dht_republish_seed_store_walk_failure => dht_republish_seed_store_walk_failures.inc();
3555 /// Record `count` stored hashes a bulk republish seed could not put to its
3556 /// origin, leaving them out of the announce set.
3557 dht_republish_seed_origin_probe_failures(count: u64) =>
3558 dht_republish_seed_origin_probe_failures.inc_by(count);
3559
3560 /// Stamp the slash watcher's `*_last_tick_timestamp_seconds` liveness gauge
3561 /// with the current wall-clock time (#1316). The `on_tick_success` hook,
3562 /// fired on EVERY successful poll tick so a dead task's gauge goes stale.
3563 slash_watcher_tick => slash_watcher_last_tick_timestamp_seconds.set(unix_now_secs());
3564 /// Record the chain-event poller's current `eth_getLogs` window span.
3565 chain_get_logs_span(span: u64) => chain_get_logs_span.set(sat_u64(span));
3566 /// Count one `eth_getLogs` window the provider rejected for its range.
3567 chain_get_logs_range_rejected => chain_get_logs_range_rejections.inc();
3568 /// Count one in-tick retry of a transiently failed `eth_getLogs` window.
3569 chain_get_logs_retried => chain_get_logs_retries.inc();
3570 /// Count one `eth_getLogs` window deferred to the next tick.
3571 chain_get_logs_deferred => chain_get_logs_deferred.inc();
3572 /// Count one boot-time chain read retried after a transient error.
3573 chain_boot_read_retried => chain_boot_read_retries.inc();
3574 /// Stamp the staker-set watcher's liveness gauge (#1316). See `slash_watcher_tick`.
3575 staker_set_watcher_tick => staker_set_watcher_last_tick_timestamp_seconds.set(unix_now_secs());
3576 /// A capacity-bond registry re-enumeration failed and the previous
3577 /// projections were kept. Bumps
3578 /// `capacity_bond_registry_resync_failures_total`. Pairs with the `warn!` in
3579 /// [`crate::dht::capacity_bond_registry`]'s `RegistrySink::on_tick_complete`.
3580 capacity_bond_registry_resync_failure => capacity_bond_registry_resync_failures.inc();
3581 /// Stamp the capacity-bond registry's re-enumeration liveness gauge on a
3582 /// successful resync. Reads `0` until the first one lands, so any alert on
3583 /// it needs a `> 0` guard.
3584 capacity_bond_registry_resync =>
3585 capacity_bond_registry_last_resync_timestamp_seconds.set(unix_now_secs());
3586 /// Stamp the blacklist watcher's liveness gauge (#1316, #1320).
3587 blacklist_watcher_tick => blacklist_watcher_last_tick_timestamp_seconds.set(unix_now_secs());
3588 /// Stamp the payment-settlement watcher's liveness gauge (#1316).
3589 settlement_watcher_tick => settlement_watcher_last_tick_timestamp_seconds.set(unix_now_secs());
3590 /// Stamp the fee-shares watcher's liveness gauge. See `slash_watcher_tick`;
3591 /// this one matters because the fee-shares watcher's failure paths
3592 /// deliberately return `Ok`, so this gauge is the only signal that it is
3593 /// still polling.
3594 fee_shares_watcher_tick => fee_shares_watcher_last_tick_timestamp_seconds.set(unix_now_secs());
3595
3596 /// Record that the slash watcher task unwound on a panic (#1316). Bumped from
3597 /// the `Drop` guard in `multiplexed_poller::run` via the `on_task_panic` hook.
3598 slash_watcher_task_panicked => slash_watcher_task_panicked.inc();
3599 /// Record that the staker-set watcher task unwound on a panic (#1316).
3600 staker_set_watcher_task_panicked => staker_set_watcher_task_panicked.inc();
3601 /// Record that the blacklist watcher task unwound on a panic (#1316, #1283).
3602 blacklist_watcher_task_panicked => blacklist_watcher_task_panicked.inc();
3603 /// Record that the fee-shares watcher task unwound on a panic.
3604 fee_shares_watcher_task_panicked => fee_shares_watcher_task_panicked.inc();
3605 /// Record that the payment-settlement watcher task unwound on a panic (#1316).
3606 settlement_watcher_task_panicked => settlement_watcher_task_panicked.inc();
3607
3608 /// Record `count` hashes a blacklist re-scope could not enforce this pass
3609 /// (#1319 — `Recheck::Failed`). One aggregated bump per pass, not per hash.
3610 blacklist_enforcement_failure(count: u64) => blacklist_enforcement_failures.inc_by(count);
3611 /// A blacklist periodic re-enumeration failed and the current deny-set was
3612 /// kept. Bumps `blacklist_reenumeration_failures_total`.
3613 blacklist_reenumeration_failure => blacklist_reenumeration_failures.inc();
3614
3615 /// Count `bytes` of payload written to a client on `cdn/client/v1`.
3616 bytes_served(bytes: u64) => bytes_served.inc_by(bytes);
3617 /// Count `bytes` of payload admitted from an upstream node in a paid pull.
3618 bytes_received(bytes: u64) => bytes_received.inc_by(bytes);
3619
3620 /// A landed `redeemMany` redeemed `vouchers` vouchers on-chain.
3621 pool_redemptions(vouchers: u64) => pool_redemptions.inc_by(vouchers);
3622 /// An inbound stream accepted a voucher as the payee.
3623 voucher_received => vouchers_received.inc();
3624 /// An inbound stream accepted a `PayWord` preimage reveal that advanced its
3625 /// lane's chain frontier.
3626 preimage_reveal_received => preimage_reveals_received.inc();
3627 /// A pool entered the owner-close grace window while this node held
3628 /// unredeemed vouchers on it.
3629 pool_grace_close => pool_grace_closes.inc();
3630 /// A node transaction mined and succeeded.
3631 onchain_tx_landed => onchain_tx_landed.inc();
3632 /// A node transaction mined and reverted.
3633 onchain_tx_reverted => onchain_tx_reverted.inc();
3634 /// The RPC refused a node transaction at `send`.
3635 onchain_tx_send_failed => onchain_tx_send_failed.inc();
3636 /// An issued node transaction's receipt wait failed and no by-hash fetch
3637 /// found the receipt.
3638 onchain_tx_receipt_failed => onchain_tx_receipt_failed.inc();
3639 /// An issued node transaction's receipt did not arrive in time and no
3640 /// by-hash fetch found it.
3641 onchain_tx_timeout => onchain_tx_timeout.inc();
3642 /// A failed or lapsed receipt wait was resolved by fetching the receipt by hash.
3643 onchain_tx_receipt_recovered => onchain_tx_receipt_recovered.inc();
3644
3645 /// A `FIND_VALUE` provider lookup started.
3646 dht_findvalue_query => dht_findvalue_queries.inc();
3647 /// A lookup round hit its timeout and aborted its in-flight RPCs.
3648 dht_lookup_round_timeout => dht_lookup_round_timeouts.inc();
3649 /// `count` peers accepted a `STORE` record from this node.
3650 dht_store_published(count: u64) => dht_store_published.inc_by(count);
3651 /// Publish the routing table's entry count.
3652 dht_routing_table_size(entries: usize) => dht_routing_table_size.set(sat(entries));
3653 /// A bucket refresh failed.
3654 dht_bucket_refresh_failure => dht_bucket_refresh_failures.inc();
3655 /// `count` bootstrap `FIND_NODE` RPCs against seeds failed.
3656 dht_bootstrap_find_node_failures(count: u64) => dht_bootstrap_find_node_failures.inc_by(count);
3657
3658 /// The fee-shares watcher's authoritative `getShares()` re-read failed.
3659 fee_shares_watcher_poll_failure => fee_shares_watcher_poll_failures.inc();
3660 /// Record whether the fee-shares watcher is unregistered because the startup
3661 /// `feeRouter()` read failed.
3662 fee_shares_watcher_unregistered(unregistered: bool) => fee_shares_watcher_unregistered.set(i64::from(unregistered));
3663
3664 /// A config reload failed and kept the previous values.
3665 config_reload_failure => config_reload_failures.inc();
3666 /// The receipt writer failed to persist one audit record.
3667 receipt_write_failure => receipt_write_failures.inc();
3668 /// A paying stream stopped mid-delivery on `PoolExhausted`.
3669 serve_stream_midstream_pool_exhausted => serve_stream_midstream_pool_exhausted.inc();
3670}
3671
3672watcher_downtime_recorders! {
3673 /// The slash-detection watcher's cycle errored and the loop is about to back
3674 /// off (#1032). Stamps `slash_watcher_down_since` (once per drift window) so
3675 /// `slash_watcher_down_seconds` climbs until the next healthy cycle. Mirrors
3676 /// [`Self::staker_set_watcher_backoff_started`]; a poisoned lock skips the
3677 /// update (the gauge then reads `i64::MAX` at scrape — the safe alerting
3678 /// direction).
3679 slash_watcher_backoff_started,
3680 /// Mark the slash-detection watcher cycle established (#1032): clear
3681 /// `slash_watcher_down_since` so `slash_watcher_down_seconds` reads `0` for
3682 /// the life of the cycle. Mirrors [`Self::staker_set_watcher_cycle_established`].
3683 slash_watcher_cycle_established,
3684 down_since: slash_watcher_down_since,
3685 restarts: slash_watcher_restarts,
3686 down_seconds: slash_watcher_down_seconds;
3687
3688 /// A staker-set watcher poll tick failed with an error and the loop is
3689 /// about to back off (#783, [`crate::dht::chain_staker_set`]).
3690 /// Stamps `down_since` so `staker_set_watcher_down_seconds` begins to climb,
3691 /// and — on the *transition* from a healthy cycle into the error state
3692 /// (`down_since` was `None`) — bumps `staker_set_watcher_restarts_total`
3693 /// exactly once per drift window, rather than once per backoff iteration of
3694 /// one continuous outage. A poisoned lock is treated as "skip the update"
3695 /// rather than panicking (anti-panic policy); the gauge's poison fallback
3696 /// (`i64::MAX`) still keeps the alert tripped. This method is the
3697 /// `on_backoff` hook wired in [`crate::dht::chain_staker_set`]; it pairs with
3698 /// the loop-level `warn!` in `multiplexed_poller::run` (`"watcher RPC error;
3699 /// restarting after backoff"`).
3700 staker_set_watcher_backoff_started,
3701 /// Mark the staker-set watcher's poll cycle as established (#783,
3702 /// downtime semantics #788): a poll tick succeeded and logs are flowing
3703 /// again (the `on_established` hook). Clears `down_since` to `None` so
3704 /// `staker_set_watcher_down_seconds`
3705 /// reads `0` for the entire life of this cycle, however long. A poisoned
3706 /// lock is treated as "skip the update" rather than panicking (anti-panic
3707 /// policy); the gauge's poison fallback (`i64::MAX`) then keeps the alert
3708 /// tripped — the safe (alerting) direction.
3709 staker_set_watcher_cycle_established,
3710 down_since: staker_set_watcher_down_since,
3711 restarts: staker_set_watcher_restarts,
3712 down_seconds: staker_set_watcher_down_seconds;
3713
3714 /// The blacklist watcher's poll tick errored and the loop is about to back
3715 /// off (#1283). Stamps `blacklist_watcher_down_since` once per drift window so
3716 /// `blacklist_watcher_down_seconds` climbs until the next healthy cycle.
3717 /// Tracks chain-read outages only; an enforcement failure that leaves a
3718 /// blacklisted blob servable is the separate `blacklist_enforcement_failures`
3719 /// counter (#1319). Mirrors [`Self::slash_watcher_backoff_started`].
3720 blacklist_watcher_backoff_started,
3721 /// Mark the blacklist watcher cycle established (#1283): clear
3722 /// `blacklist_watcher_down_since` so `blacklist_watcher_down_seconds` reads
3723 /// `0` for the life of the cycle. Mirrors [`Self::slash_watcher_cycle_established`].
3724 blacklist_watcher_cycle_established,
3725 down_since: blacklist_watcher_down_since,
3726 restarts: blacklist_watcher_restarts,
3727 down_seconds: blacklist_watcher_down_seconds;
3728
3729 /// The payment-settlement watcher's poll tick errored and the loop is about
3730 /// to back off (#1316). Stamps `settlement_watcher_down_since` once per drift
3731 /// window. Mirrors [`Self::slash_watcher_backoff_started`].
3732 settlement_watcher_backoff_started,
3733 /// Mark the payment-settlement watcher cycle established (#1316): clear
3734 /// `settlement_watcher_down_since`. Mirrors [`Self::slash_watcher_cycle_established`].
3735 settlement_watcher_cycle_established,
3736 down_since: settlement_watcher_down_since,
3737 restarts: settlement_watcher_restarts,
3738 down_seconds: settlement_watcher_down_seconds;
3739
3740 /// The fee-shares watcher's poll tick errored and the loop is about to back
3741 /// off. Stamps `fee_shares_watcher_down_since` once per drift window.
3742 /// Mirrors [`Self::slash_watcher_backoff_started`].
3743 fee_shares_watcher_backoff_started,
3744 /// Mark the fee-shares watcher cycle established: clear
3745 /// `fee_shares_watcher_down_since`. Mirrors [`Self::slash_watcher_cycle_established`].
3746 fee_shares_watcher_cycle_established,
3747 down_since: fee_shares_watcher_down_since,
3748 restarts: fee_shares_watcher_restarts,
3749 down_seconds: fee_shares_watcher_down_seconds;
3750}
3751
3752/// Bind the `/metrics` HTTP listener synchronously so startup can fail fast
3753/// if the port is unavailable. The returned listener is consumed by [`serve`].
3754///
3755/// Emits a `WARN` if `addr` is non-loopback (#579). The `OpenMetrics`
3756/// surface exposes pull-through byte volumes and GC/connection stats —
3757/// useful reconnaissance for anyone who can reach it. The default config
3758/// binds loopback (`appendix-local-admin-http` calls metrics
3759/// "loopback-only"), but `observability.metrics_bind` is operator-
3760/// settable to `0.0.0.0` for containerised deployments
3761/// (`crates/common/src/config/types.rs` — `ObservabilityConfig::metrics_bind`).
3762/// We warn but do not reject so that documented container workflows
3763/// keep working.
3764///
3765/// # Errors
3766///
3767/// Returns an error if the underlying `crate::net::bind_reuseaddr` fails —
3768/// socket creation, `bind` (port in use, permissions), or `listen`.
3769pub fn bind(addr: SocketAddr) -> anyhow::Result<TcpListener> {
3770 // `SO_REUSEADDR` so a restart rebinds this fixed port immediately instead of
3771 // racing a `TIME_WAIT` remnant from the prior process (see `crate::net`).
3772 let listener = crate::net::bind_reuseaddr(addr)
3773 .map_err(|e| anyhow::anyhow!("metrics bind {addr} failed: {e}"))?;
3774 // `to_canonical()` unwraps IPv4-mapped IPv6 (e.g.
3775 // `::ffff:127.0.0.1`) so an operator binding the dual-stack
3776 // form of loopback doesn't get a false "non-loopback" warning.
3777 // `Ipv6Addr::is_loopback()` only matches `::1`.
3778 let ip = addr.ip().to_canonical();
3779 if !ip.is_loopback() {
3780 // `is_unspecified()` (`0.0.0.0` / `::`) is the common
3781 // containerised case; we call it out by name so an operator
3782 // grepping startup logs sees the intent. A public IP falls
3783 // through to the generic non-loopback message.
3784 if ip.is_unspecified() {
3785 tracing::warn!(
3786 %addr,
3787 "metrics server is binding all interfaces (non-loopback); the OpenMetrics \
3788 endpoint exposes pull-through byte volumes and GC/connection stats — gate \
3789 it behind a private network or reverse proxy if reachable from outside the \
3790 host"
3791 );
3792 } else {
3793 tracing::warn!(
3794 %addr,
3795 "metrics server is binding a non-loopback address; the OpenMetrics \
3796 endpoint exposes pull-through byte volumes and GC/connection stats — \
3797 restrict reachability to trusted scrapers"
3798 );
3799 }
3800 }
3801 tracing::info!(%addr, "metrics server listening");
3802 Ok(listener)
3803}
3804
3805/// Serve `/metrics` over HTTP on the pre-bound `listener` until `shutdown`
3806/// fires. The shutdown receiver is consumed; send `()` to stop the accept
3807/// loop.
3808///
3809/// Per-connection tasks are spawned with `tokio::spawn` and **detached** —
3810/// they are not tracked or awaited during shutdown. In practice scrapes
3811/// complete in milliseconds, and dropping an in-flight `/metrics` response
3812/// is harmless (the scraper will retry on its next interval). This is a
3813/// deliberate choice: tracking an unbounded `JoinSet` alongside the accept
3814/// loop would add complexity without a consumer that cares about the
3815/// guarantee.
3816#[allow(clippy::cognitive_complexity)] // Accept+permit+spawn reads linearly.
3817pub async fn serve(
3818 listener: TcpListener,
3819 metrics: Arc<Metrics>,
3820 mut shutdown: oneshot::Receiver<()>,
3821) -> anyhow::Result<()> {
3822 let limiter = Arc::new(Semaphore::new(MAX_METRICS_CONNECTIONS));
3823
3824 loop {
3825 let (stream, peer) = tokio::select! {
3826 biased;
3827 _ = &mut shutdown => {
3828 tracing::debug!("metrics server shutdown signal received");
3829 return Ok(());
3830 }
3831 res = listener.accept() => match res {
3832 Ok(pair) => pair,
3833 Err(err) => {
3834 tracing::warn!(error = %err, "metrics accept failed");
3835 continue;
3836 }
3837 },
3838 };
3839
3840 let Ok(permit) = Arc::clone(&limiter).try_acquire_owned() else {
3841 tracing::warn!(
3842 %peer,
3843 limit = MAX_METRICS_CONNECTIONS,
3844 "metrics connection rejected: at capacity",
3845 );
3846 drop(stream);
3847 continue;
3848 };
3849
3850 let metrics = Arc::clone(&metrics);
3851 tokio::spawn(async move {
3852 let _permit = permit; // released when task finishes
3853 let io = TokioIo::new(stream);
3854 let svc = service_fn(move |req| {
3855 let metrics = Arc::clone(&metrics);
3856 async move { handle(req, metrics) }
3857 });
3858 if let Err(err) = hyper::server::conn::http1::Builder::new()
3859 .serve_connection(io, svc)
3860 .await
3861 {
3862 tracing::debug!(error = %err, "metrics connection ended");
3863 }
3864 });
3865 }
3866}
3867
3868#[allow(clippy::unnecessary_wraps, clippy::needless_pass_by_value)] // hyper service_fn signature requires Result and owned req.
3869fn handle(
3870 req: Request<hyper::body::Incoming>,
3871 metrics: Arc<Metrics>,
3872) -> Result<Response<Full<Bytes>>, hyper::Error> {
3873 if req.uri().path() != "/metrics" {
3874 return Ok(Response::builder()
3875 .status(StatusCode::NOT_FOUND)
3876 .body(Full::new(Bytes::from_static(b"not found\n")))
3877 .unwrap_or_else(|_| Response::new(Full::new(Bytes::new()))));
3878 }
3879
3880 match metrics.encode() {
3881 Ok(body) => Ok(Response::builder()
3882 .status(StatusCode::OK)
3883 .header(
3884 "content-type",
3885 "application/openmetrics-text; version=1.0.0; charset=utf-8",
3886 )
3887 .body(Full::new(Bytes::from(body)))
3888 .unwrap_or_else(|_| Response::new(Full::new(Bytes::new())))),
3889 Err(err) => {
3890 tracing::warn!(error = %err, "metrics encode error");
3891 Ok(Response::builder()
3892 .status(StatusCode::INTERNAL_SERVER_ERROR)
3893 .body(Full::new(Bytes::from_static(b"encode error\n")))
3894 .unwrap_or_else(|_| Response::new(Full::new(Bytes::new()))))
3895 }
3896 }
3897}
3898
3899/// RAII guard for the `active_connections` gauge.
3900///
3901/// Increments the gauge on construction, decrements on drop — so the count
3902/// stays correct even if the handler future is cancelled (e.g. during
3903/// shutdown) between open and close.
3904#[derive(Debug)]
3905pub struct ConnectionGuard<'a> {
3906 metrics: &'a Metrics,
3907}
3908
3909impl<'a> ConnectionGuard<'a> {
3910 fn new(metrics: &'a Metrics) -> Self {
3911 metrics.connection_opened();
3912 Self { metrics }
3913 }
3914}
3915
3916impl Drop for ConnectionGuard<'_> {
3917 fn drop(&mut self) {
3918 self.metrics.connection_closed();
3919 }
3920}
3921
3922/// One-shot time-to-first-byte clock for one inbound paid serve.
3923///
3924/// The clock starts when the request is decoded and records into the
3925/// `decdn_serve_first_byte_{hit,miss}_seconds` sibling that matches the
3926/// [`RequestClass`] the availability gate admitted the serve under. A partial
3927/// hit is a [`RequestClass::CacheHit`]. [`Self::record`] consumes the clock, so a stream
3928/// records at most once. A stream that ends before its first frame drops the
3929/// clock and records nothing.
3930///
3931/// The clock also splits the total into three phases: admission (decoded to
3932/// load-shed admit), response (admit to the signed `StreamResponse` written)
3933/// and first frame (response written to the first frame written). Each phase
3934/// records into its own histogram, and a `debug` event carries all three.
3935#[derive(Debug)]
3936pub(crate) struct FirstByteClock {
3937 started: Instant,
3938 admitted: Instant,
3939 responded: Option<Instant>,
3940 class: RequestClass,
3941}
3942
3943impl FirstByteClock {
3944 /// A clock that started at `started` and passed the load-shed gate at
3945 /// `admitted`, for a serve of class `class`.
3946 pub(crate) const fn new(started: Instant, admitted: Instant, class: RequestClass) -> Self {
3947 Self {
3948 started,
3949 admitted,
3950 responded: None,
3951 class,
3952 }
3953 }
3954
3955 /// Mark the signed `StreamResponse` written.
3956 pub(crate) fn mark_responded(&mut self) {
3957 self.responded = Some(Instant::now());
3958 }
3959
3960 /// Record the time since the clock started and each phase: the first frame
3961 /// is written. Without a response mark, only the admission phase and the
3962 /// total record.
3963 pub(crate) fn record(self, metrics: &Metrics) {
3964 let now = Instant::now();
3965 let total = now.saturating_duration_since(self.started);
3966 let admission = self.admitted.saturating_duration_since(self.started);
3967 let m = &metrics.decdn;
3968 m.serve_admission_seconds.observe(admission.as_secs_f64());
3969 let phases = self.responded.map(|responded| {
3970 (
3971 responded.saturating_duration_since(self.admitted),
3972 now.saturating_duration_since(responded),
3973 )
3974 });
3975 let (first_byte, response, first_frame) = match self.class {
3976 RequestClass::CacheHit => (
3977 &m.serve_first_byte_hit_seconds,
3978 &m.serve_response_hit_seconds,
3979 &m.serve_first_frame_hit_seconds,
3980 ),
3981 RequestClass::CacheMiss => (
3982 &m.serve_first_byte_miss_seconds,
3983 &m.serve_response_miss_seconds,
3984 &m.serve_first_frame_miss_seconds,
3985 ),
3986 };
3987 first_byte.observe(total.as_secs_f64());
3988 if let Some((resp, frame)) = phases {
3989 response.observe(resp.as_secs_f64());
3990 first_frame.observe(frame.as_secs_f64());
3991 }
3992 tracing::debug!(
3993 class = ?self.class,
3994 total_ms = total.as_millis(),
3995 admission_ms = admission.as_millis(),
3996 response_ms = phases.map(|(r, _)| r.as_millis()),
3997 first_frame_ms = phases.map(|(_, f)| f.as_millis()),
3998 "serve first byte"
3999 );
4000 }
4001}
4002
4003/// Drop-safe accounting for a paid delivery stream in one direction.
4004#[derive(Debug)]
4005pub(crate) struct StreamGuard {
4006 gauge: Arc<Gauge>,
4007}
4008
4009impl StreamGuard {
4010 fn new(gauge: Arc<Gauge>) -> Self {
4011 gauge.inc();
4012 Self { gauge }
4013 }
4014}
4015
4016impl Drop for StreamGuard {
4017 fn drop(&mut self) {
4018 self.gauge.dec();
4019 }
4020}
4021
4022#[cfg(test)]
4023#[allow(clippy::unwrap_used)]
4024mod tests;