saluki_app/metrics/
mod.rs

1//! Metrics.
2
3use std::{sync::OnceLock, time::Duration};
4
5use async_trait::async_trait;
6use metrics::{gauge, Gauge, Level};
7use saluki_common::sync::shutdown::ShutdownHandle;
8use saluki_core::{
9    observability::metrics::MetricsFlusherWorker,
10    runtime::{InitializationError, Supervisable, SupervisorFuture},
11};
12use saluki_error::GenericError;
13use saluki_metrics::static_metrics;
14use tokio::{runtime::Handle, select, time::sleep};
15
16mod api;
17pub use self::api::{MetricsAPIHandler, MetricsOverrideWorker};
18
19/// The set of workers spawned by [`initialize_metrics`].
20///
21/// Each worker must be added to a [`Supervisor`][saluki_core::runtime::Supervisor] for the metrics subsystem to
22/// fully function: the flusher worker propagates internal metrics to subscribers, the runtime worker emits Tokio
23/// runtime gauges, and the override processor asserts the privileged API routes and handles dynamic filter
24/// overrides driven through them.
25pub(crate) struct MetricsWorkers {
26    pub runtime: RuntimeMetricsWorker,
27    pub flusher: MetricsFlusherWorker,
28    pub override_processor: MetricsOverrideWorker,
29}
30
31/// Initializes the metrics subsystem for `metrics`.
32///
33/// The given prefix is used to namespace all metrics that are emitted by the application, and is prepended to all
34/// metrics, followed by a period (for example, `<prefix>.<metric name>`). The given default level seeds the runtime
35/// filter and is what the filter is restored to when [`MetricsAPIHandler`]'s reset route is invoked.
36///
37/// Returns a [`MetricsWorkers`] bundle containing the supervisable workers needed to drive the metrics subsystem
38/// at runtime.
39///
40/// # Errors
41///
42/// If the metrics subsystem was already initialized, an error will be returned.
43pub(crate) async fn initialize_metrics(
44    metrics_prefix: impl Into<String>, default_level: Level,
45) -> Result<MetricsWorkers, GenericError> {
46    // We forward to the implementation in `saluki_core` so that we can have this crate be the collection point of all
47    // helpers/types that are specific to generic application setup/initialization.
48    //
49    // The implementation itself has to live in `saluki_core`, however, to have access to all of the underlying types
50    // that are created and used to install the global recorder, such that they need not be exposed publicly.
51    let (filter_handle, flusher) =
52        saluki_core::observability::metrics::initialize_metrics(metrics_prefix.into(), default_level).await?;
53
54    let override_processor = MetricsOverrideWorker::new(filter_handle);
55
56    // Capture the current runtime handle eagerly so the runtime metrics worker measures the runtime that owns
57    // bootstrap, regardless of where the worker future eventually executes under the supervisor.
58    let runtime = RuntimeMetricsWorker::new("primary", Handle::current());
59
60    Ok(MetricsWorkers {
61        runtime,
62        flusher,
63        override_processor,
64    })
65}
66
67/// Emits the startup metrics for the application.
68///
69/// This is generally meant to be called after the application has been initialized, in order to indicate the
70/// application has completed start-up and is now running.
71///
72/// Must be called after the metrics subsystem has been initialized.
73pub fn emit_startup_metrics() {
74    // We hold the handle for the life of the process so it doesn't get idle reaped.
75    static RUNNING: OnceLock<Gauge> = OnceLock::new();
76
77    let app_details = saluki_metadata::get_app_details();
78    let app_version = if app_details.is_dev_build() {
79        format!("{}-dev-{}", app_details.version().raw(), app_details.git_hash(),)
80    } else {
81        app_details.version().raw().to_string()
82    };
83
84    // Emit a "running" metric to indicate that the application is running.
85    let running = RUNNING.get_or_init(|| gauge!("running", "version" => app_version));
86    running.set(1.0);
87}
88
89/// Collects Tokio runtime metrics from the given runtime handle.
90///
91/// All metrics generated will include a `runtime_id` label which maps to the given runtime ID. This allows for
92/// differentiating between multiple runtimes that may be running in the same process.
93pub async fn collect_runtime_metrics(runtime_id: &str, handle: Handle) {
94    // Grab the total number of runtime workers to properly initialize/register our metrics.
95    let runtime_metrics = RuntimeMetrics::with_workers(runtime_id, handle.metrics().num_workers());
96
97    // With our metrics registered, enter the main loop where we periodically scrape the metrics.
98    loop {
99        let latest_runtime_metrics = handle.metrics();
100        runtime_metrics.update(&latest_runtime_metrics);
101
102        sleep(Duration::from_secs(5)).await;
103    }
104}
105
106/// A worker that periodically collects Tokio runtime metrics.
107///
108/// The runtime is captured at construction time so that the metrics measured always describe the runtime that
109/// owned the bootstrap, regardless of where this worker eventually executes under a supervisor.
110pub struct RuntimeMetricsWorker {
111    runtime_id: String,
112    handle: Handle,
113}
114
115impl RuntimeMetricsWorker {
116    /// Creates a new `RuntimeMetricsWorker` that collects metrics from the given runtime.
117    pub fn new<S: Into<String>>(runtime_id: S, handle: Handle) -> Self {
118        Self {
119            runtime_id: runtime_id.into(),
120            handle,
121        }
122    }
123}
124
125#[async_trait]
126impl Supervisable for RuntimeMetricsWorker {
127    fn name(&self) -> &str {
128        "tokio-runtime-metrics-collector"
129    }
130
131    async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError> {
132        let runtime_id = self.runtime_id.clone();
133        let handle = self.handle.clone();
134
135        Ok(Box::pin(async move {
136            select! {
137                _ = process_shutdown => {},
138                _ = collect_runtime_metrics(&runtime_id, handle) => {},
139            }
140
141            Ok(())
142        }))
143    }
144}
145
146#[static_metrics(prefix = runtime_worker, labels(runtime_id, worker_idx))]
147#[derive(Clone)]
148struct WorkerMetrics {
149    #[metric(level = trace)]
150    local_queue_depth: Gauge,
151    #[metric(level = trace)]
152    local_schedule_count: Gauge,
153    #[metric(level = trace)]
154    mean_poll_time: Gauge,
155    #[metric(level = trace)]
156    noop_count: Gauge,
157    #[metric(level = trace)]
158    overflow_count: Gauge,
159    #[metric(level = trace)]
160    park_count: Gauge,
161    #[metric(level = trace)]
162    park_unpark_count: Gauge,
163    #[metric(level = trace)]
164    poll_count: Gauge,
165    #[metric(level = trace)]
166    steal_count: Gauge,
167    #[metric(level = trace)]
168    steal_operations: Gauge,
169    #[metric(level = trace)]
170    total_busy_duration: Gauge,
171}
172
173impl WorkerMetrics {
174    fn with_worker_idx(runtime_id: &str, worker_idx: usize) -> Self {
175        Self::new(runtime_id, worker_idx)
176    }
177
178    fn update(&self, worker_idx: usize, metrics: &tokio::runtime::RuntimeMetrics) {
179        self.local_queue_depth()
180            .set(metrics.worker_local_queue_depth(worker_idx) as f64);
181        self.local_schedule_count()
182            .set(metrics.worker_local_schedule_count(worker_idx) as f64);
183        self.mean_poll_time()
184            .set(metrics.worker_mean_poll_time(worker_idx).as_nanos() as f64);
185        self.noop_count().set(metrics.worker_noop_count(worker_idx) as f64);
186        self.overflow_count()
187            .set(metrics.worker_overflow_count(worker_idx) as f64);
188        self.park_count().set(metrics.worker_park_count(worker_idx) as f64);
189        self.park_unpark_count()
190            .set(metrics.worker_park_unpark_count(worker_idx) as f64);
191        self.poll_count().set(metrics.worker_poll_count(worker_idx) as f64);
192        self.steal_count().set(metrics.worker_steal_count(worker_idx) as f64);
193        self.steal_operations()
194            .set(metrics.worker_steal_operations(worker_idx) as f64);
195        self.total_busy_duration()
196            .set(metrics.worker_total_busy_duration(worker_idx).as_nanos() as f64);
197    }
198}
199
200#[static_metrics(prefix = runtime, labels(runtime_id))]
201#[derive(Clone)]
202struct GlobalRuntimeMetrics {
203    #[metric(level = debug)]
204    num_alive_tasks: Gauge,
205    #[metric(level = debug)]
206    blocking_queue_depth: Gauge,
207    #[metric(level = debug)]
208    budget_forced_yield_count: Gauge,
209    #[metric(level = debug)]
210    global_queue_depth: Gauge,
211    #[metric(level = debug)]
212    io_driver_fd_deregistered_count: Gauge,
213    #[metric(level = debug)]
214    io_driver_fd_registered_count: Gauge,
215    #[metric(level = debug)]
216    io_driver_ready_count: Gauge,
217    #[metric(level = debug)]
218    num_blocking_threads: Gauge,
219    #[metric(level = debug)]
220    num_idle_blocking_threads: Gauge,
221    #[metric(level = debug)]
222    num_workers: Gauge,
223    #[metric(level = debug)]
224    remote_schedule_count: Gauge,
225    #[metric(level = debug)]
226    spawned_tasks_count: Gauge,
227}
228
229struct RuntimeMetrics {
230    global: GlobalRuntimeMetrics,
231    workers: Vec<WorkerMetrics>,
232}
233
234impl RuntimeMetrics {
235    fn with_workers(runtime_id: &str, workers_len: usize) -> Self {
236        let mut workers = Vec::with_capacity(workers_len);
237        for i in 0..workers_len {
238            workers.push(WorkerMetrics::with_worker_idx(runtime_id, i));
239        }
240
241        Self {
242            global: GlobalRuntimeMetrics::new(runtime_id),
243            workers,
244        }
245    }
246
247    fn update(&self, metrics: &tokio::runtime::RuntimeMetrics) {
248        self.global.num_alive_tasks().set(metrics.num_alive_tasks() as f64);
249        self.global
250            .blocking_queue_depth()
251            .set(metrics.blocking_queue_depth() as f64);
252        self.global
253            .budget_forced_yield_count()
254            .set(metrics.budget_forced_yield_count() as f64);
255        self.global
256            .global_queue_depth()
257            .set(metrics.global_queue_depth() as f64);
258        self.global
259            .io_driver_fd_deregistered_count()
260            .set(metrics.io_driver_fd_deregistered_count() as f64);
261        self.global
262            .io_driver_fd_registered_count()
263            .set(metrics.io_driver_fd_registered_count() as f64);
264        self.global
265            .io_driver_ready_count()
266            .set(metrics.io_driver_ready_count() as f64);
267        self.global
268            .num_blocking_threads()
269            .set(metrics.num_blocking_threads() as f64);
270        self.global
271            .num_idle_blocking_threads()
272            .set(metrics.num_idle_blocking_threads() as f64);
273        self.global.num_workers().set(metrics.num_workers() as f64);
274        self.global
275            .remote_schedule_count()
276            .set(metrics.remote_schedule_count() as f64);
277        self.global
278            .spawned_tasks_count()
279            .set(metrics.spawned_tasks_count() as f64);
280
281        for (worker_idx, worker) in self.workers.iter().enumerate() {
282            worker.update(worker_idx, metrics);
283        }
284    }
285}