Skip to main content

iota_http/metrics/
mod.rs

1// Copyright (c) 2026 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4//! Measures the connections of one listener.
5//!
6//! The listener wraps each socket it accepts in a `TrackedIo` and uses it for
7//! the whole life of the connection.
8
9use std::{
10    fmt,
11    net::SocketAddr,
12    sync::{Arc, OnceLock},
13};
14
15use iota_metrics::peak::IntGaugeWithPeakGauge;
16use prometheus_filtered::{
17    Histogram, IntCounterVec, MetricLevel, Registry, register_histogram_with_registry,
18    register_int_counter_vec_with_registry,
19};
20
21mod connection;
22mod io;
23mod scope;
24mod tls;
25
26pub(crate) use connection::ConnectionGuard;
27pub(crate) use io::TrackedIo;
28use scope::Scope;
29pub(crate) use tls::HandshakeResult;
30
31/// Connection lifetimes in seconds.
32const CONNECTION_LIFETIME_SEC_BUCKETS: &[f64] =
33    &[1.0, 5.0, 30.0, 60.0, 300.0, 1800.0, 14400.0, 86400.0];
34
35/// The metrics of one listener, and the state they need.
36#[derive(Clone)]
37pub struct ListenerMetrics(Arc<Inner>);
38
39impl fmt::Debug for ListenerMetrics {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        f.debug_struct("ListenerMetrics")
42            .field("listener", &self.0.prefix)
43            .finish_non_exhaustive()
44    }
45}
46
47impl ListenerMetrics {
48    /// Registers the metrics as `{prefix}_<name>` in `registry`, all at
49    /// `level`. The TLS metrics are registered later, by `enable_tls`.
50    ///
51    /// # Panics
52    ///
53    /// Panics if the same `prefix` is registered twice in one registry.
54    pub fn new(prefix: &str, registry: &Registry, level: MetricLevel) -> Self {
55        Self(Arc::new(Inner {
56            prefix: prefix.to_owned(),
57            registry: registry.clone(),
58            level,
59            metrics: Metrics::new(prefix, registry, level),
60            tls: OnceLock::new(),
61        }))
62    }
63
64    /// Registers the TLS metrics. The server calls it when it starts with TLS;
65    /// a second call does nothing.
66    pub(crate) fn enable_tls(&self) {
67        self.0.tls.get_or_init(|| {
68            Arc::new(tls::Metrics::new(
69                &self.0.prefix,
70                &self.0.registry,
71                self.0.level,
72            ))
73        });
74    }
75
76    /// Counts a socket the listener has just accepted, before any TLS
77    /// handshake.
78    fn record_accept(&self, remote: SocketAddr) -> ConnectionGuard {
79        let metrics = &self.0.metrics;
80        let scope: &str = Scope::of(remote.ip()).into();
81        metrics
82            .inbound_connections_accepted
83            .with_label_values(&[scope])
84            .inc();
85        metrics.inbound_connections.inc();
86        ConnectionGuard::new(self.0.clone())
87    }
88}
89
90struct Inner {
91    prefix: String,
92    registry: Registry,
93    level: MetricLevel,
94    metrics: Metrics,
95    /// Registered when the listener starts with TLS.
96    tls: OnceLock<Arc<tls::Metrics>>,
97}
98
99struct Metrics {
100    inbound_connections_accepted: IntCounterVec,
101    inbound_connections: IntGaugeWithPeakGauge,
102    connection_lifetime_seconds: Histogram,
103}
104
105impl Metrics {
106    fn new(prefix: &str, registry: &Registry, level: MetricLevel) -> Self {
107        Self {
108            inbound_connections_accepted: register_int_counter_vec_with_registry!(
109                format!("{prefix}_inbound_connections_accepted"),
110                "Sockets accepted, counted from the TCP accept, before any TLS handshake. \
111                 The scope is where the remote address is: loopback, private or public",
112                &["scope"],
113                registry;
114                level
115            )
116            .expect("the metrics of a listener register without collision"),
117            inbound_connections: IntGaugeWithPeakGauge::register(
118                (
119                    &format!("{prefix}_inbound_connections"),
120                    "The number of connections open now. A connection counts from its TCP accept \
121                     to its close, TLS handshake included",
122                ),
123                (
124                    &format!("{prefix}_inbound_connections_peak"),
125                    "The most open connections over the last 2 minutes. A scrape reports at \
126                     least the current number",
127                ),
128                module_path!(),
129                registry,
130                level,
131            ),
132            connection_lifetime_seconds: register_histogram_with_registry!(
133                format!("{prefix}_connection_lifetime_seconds"),
134                "Time from the TCP accept to the close, in seconds",
135                CONNECTION_LIFETIME_SEC_BUCKETS.to_vec(),
136                registry;
137                level
138            )
139            .expect("the metrics of a listener register without collision"),
140        }
141    }
142}
143
144#[cfg(test)]
145mod tests;