Skip to main content

postrust_engine/
observe.rs

1//! Measurements the pipeline takes for a host that exports metrics.
2//!
3//! The engine is shared with hosts that have no metrics pipeline -- a
4//! Cloudflare Worker, a test -- and builds for wasm32, so it does not depend on
5//! OpenTelemetry or any other metrics crate. Instead a host that does export
6//! metrics installs an [`Observer`] once, at start-up, and the pipeline calls
7//! it with what it measured.
8//!
9//! # What it costs when nothing is installed
10//!
11//! One load of an initialised-or-not flag per measurement point, and nothing
12//! else: the clock is not even read unless an observer is there to be told.
13//! That is the point of doing it this way rather than always timing and
14//! throwing the result away -- a request that nobody is measuring pays for
15//! nothing it does not use.
16//!
17//! # Why a process-wide hook
18//!
19//! Metrics are process-wide anyway: an OpenTelemetry meter provider and a
20//! Prometheus registry are each one per process, and so is the endpoint a
21//! scraper reads. Threading an observer through [`State`](crate::State) would
22//! give every host a method to implement for something only one of them uses.
23
24use std::sync::OnceLock;
25use std::time::Duration;
26// `std::time` natively; on wasm32, where `Instant::now()` panics, the
27// JavaScript host's clock.
28use web_time::Instant;
29
30/// What a host wants to be told about. Every method has a default that
31/// ignores the measurement, so an observer names only what it records, and a
32/// measurement added here later breaks no host's build.
33pub trait Observer: Send + Sync + 'static {
34    /// How long a request waited for a transaction to begin: a connection
35    /// from the pool, and the `BEGIN` sent on it. Mostly the pool -- `BEGIN`
36    /// is one round trip -- so a wait that grows is a pool that is too small
37    /// for the load, or a database that is slow to hand connections back.
38    fn transaction_begun(&self, waited: Duration) {
39        let _ = waited;
40    }
41}
42
43static OBSERVER: OnceLock<Box<dyn Observer>> = OnceLock::new();
44
45/// Install the observer, once per process.
46///
47/// Returns the observer back if one is already installed: the first one
48/// stays, since measurements already sent to it would otherwise be split
49/// between two pipelines.
50pub fn set_observer(observer: impl Observer) -> Result<(), Box<dyn Observer>> {
51    OBSERVER.set(Box::new(observer))
52}
53
54/// A timer for the wait for a transaction, started only if somebody will be
55/// told what it measured.
56pub(crate) fn begin_timer() -> Option<Instant> {
57    OBSERVER.get().map(|_| Instant::now())
58}
59
60/// Report what [`begin_timer`] started.
61pub(crate) fn transaction_begun(started: Option<Instant>) {
62    if let (Some(observer), Some(started)) = (OBSERVER.get(), started) {
63        observer.transaction_begun(started.elapsed());
64    }
65}