Skip to main content

pg_embedded_setup_unpriv/
observability.rs

1//! Shared tracing configuration and metric recording for observability.
2//!
3//! Centralizes the log target used by the crate so subscribers can filter
4//! observability events without pulling in unrelated application logs, and
5//! provides the seam through which a consumer collects counts.
6//!
7//! # Why a seam rather than a metrics crate
8//!
9//! Every subsystem here reports through bounded `tracing` events, which a
10//! consumer can filter but not readily aggregate. A metric is the other thing:
11//! a count to add up without parsing logs. This crate takes no metrics
12//! dependency, because a library should not choose one on its consumer's
13//! behalf; a consumer installs a [`MetricsRecorder`] and forwards each count
14//! to whatever it already runs. With no recorder installed, recording is a
15//! branch and a return.
16
17use std::sync::{Arc, PoisonError, RwLock};
18
19/// Target used by observability spans and logs.
20pub(crate) const LOG_TARGET: &str = "pg_embed::observability";
21
22/// Outcome of one password-reuse decision.
23///
24/// This is an enum rather than a string so the label set is bounded by
25/// construction: a caller cannot route a password, a path, or any other
26/// unbounded value into a metric through it. `ProbeFailed` and
27/// `UnreadableFile` stay distinct even though both map to
28/// [`BootstrapErrorKind::ClusterPasswordUnreadable`](crate::BootstrapErrorKind),
29/// because failing to search the data directory and failing to read the
30/// password file are different operational problems.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32#[non_exhaustive]
33pub enum PasswordReuseOutcomeMetric {
34    /// The stored password was adopted.
35    Reused,
36    /// The caller supplied `PG_PASSWORD`, so the file was never read.
37    ExplicitPassword,
38    /// The data directory holds no cluster.
39    NoCluster,
40    /// The `PG_VERSION` marker could not be probed.
41    ProbeFailed,
42    /// The cluster exists but its password file is absent.
43    MissingFile,
44    /// The password file exists but could not be read.
45    UnreadableFile,
46    /// The password file exists but is empty.
47    EmptyFile,
48}
49
50/// A count this crate records.
51///
52/// Every variant carries a bounded label set and nothing else. Marked
53/// `#[non_exhaustive]` so later releases can add counts without breaking a
54/// consumer's `match`.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
56#[non_exhaustive]
57pub enum Metric {
58    /// One password-reuse decision, recorded once per bootstrap that consults
59    /// the stored password.
60    PasswordReuse(PasswordReuseOutcomeMetric),
61}
62
63/// Receives each [`Metric`] the crate records.
64///
65/// # Examples
66///
67/// ```
68/// use std::sync::{Arc, Mutex};
69///
70/// use pg_embedded_setup_unpriv::observability::{
71///     Metric,
72///     MetricsRecorder,
73///     install_metrics_recorder,
74/// };
75///
76/// #[derive(Default)]
77/// struct Collected(Mutex<Vec<Metric>>);
78///
79/// impl MetricsRecorder for Collected {
80///     fn record(&self, metric: Metric) {
81///         self.0
82///             .lock()
83///             .unwrap_or_else(|err| err.into_inner())
84///             .push(metric);
85///     }
86/// }
87///
88/// let recorder = Arc::new(Collected::default());
89/// let _guard = install_metrics_recorder(Arc::clone(&recorder) as Arc<dyn MetricsRecorder>);
90/// // The guard restores the previous recorder when it drops.
91/// ```
92pub trait MetricsRecorder: Send + Sync {
93    /// Records one count. Implementations must not panic.
94    fn record(&self, metric: Metric);
95}
96
97/// The installed recorder, if any.
98static RECORDER: RwLock<Option<Arc<dyn MetricsRecorder>>> = RwLock::new(None);
99
100/// Restores the previous recorder when dropped.
101///
102/// Installing returns a guard rather than being permanent so a test can
103/// install a collector without leaking it into the next test, matching how
104/// this crate's other process-wide hooks behave.
105#[must_use = "the recorder is uninstalled when the guard drops"]
106pub struct MetricsRecorderGuard {
107    previous: Option<Arc<dyn MetricsRecorder>>,
108}
109
110impl Drop for MetricsRecorderGuard {
111    fn drop(&mut self) {
112        let mut slot = RECORDER.write().unwrap_or_else(PoisonError::into_inner);
113        *slot = self.previous.take();
114    }
115}
116
117/// Installs `recorder` and returns a guard that restores the previous one.
118///
119/// Installation is process-wide and nests: the guard puts back whatever was
120/// installed before it, so an inner scope cannot strand an outer collector.
121///
122/// # Examples
123///
124/// ```
125/// use std::sync::{Arc, Mutex};
126///
127/// use pg_embedded_setup_unpriv::observability::{
128///     Metric,
129///     MetricsRecorder,
130///     install_metrics_recorder,
131/// };
132///
133/// #[derive(Default)]
134/// struct Counting(Mutex<usize>);
135///
136/// impl MetricsRecorder for Counting {
137///     fn record(&self, _metric: Metric) {
138///         *self.0.lock().unwrap_or_else(|err| err.into_inner()) += 1;
139///     }
140/// }
141///
142/// let outer = Arc::new(Counting::default());
143/// let outer_guard = install_metrics_recorder(Arc::clone(&outer) as Arc<dyn MetricsRecorder>);
144///
145/// let inner = Arc::new(Counting::default());
146/// let inner_guard = install_metrics_recorder(Arc::clone(&inner) as Arc<dyn MetricsRecorder>);
147/// // Counts recorded here reach `inner`, not `outer`.
148/// drop(inner_guard);
149/// // `outer` is installed again from here until `outer_guard` drops.
150/// drop(outer_guard);
151/// // Nothing is installed now, and recording is a branch and a return.
152/// ```
153pub fn install_metrics_recorder(recorder: Arc<dyn MetricsRecorder>) -> MetricsRecorderGuard {
154    let mut slot = RECORDER.write().unwrap_or_else(PoisonError::into_inner);
155    let previous = slot.replace(recorder);
156    MetricsRecorderGuard { previous }
157}
158
159/// Records `metric` with the installed recorder, or does nothing.
160///
161/// The installed handle is cloned out and the read lock released before the
162/// consumer's `record` runs. Holding the lock across that call would deadlock
163/// any recorder that installs another recorder or drops a guard from inside
164/// its callback, because both take the write lock on the same thread.
165pub(crate) fn record(metric: Metric) {
166    let installed = {
167        let slot = RECORDER.read().unwrap_or_else(PoisonError::into_inner);
168        slot.as_ref().map(Arc::clone)
169    };
170    if let Some(recorder) = installed {
171        recorder.record(metric);
172    }
173}
174
175#[cfg(test)]
176#[path = "observability_tests.rs"]
177mod tests;