Skip to main content

varnish/
metrics_reader.rs

1use std::collections::HashMap;
2use std::ffi::{c_char, c_int, c_void, CStr, CString, NulError};
3use std::marker::PhantomData;
4use std::path::Path;
5use std::ptr;
6use std::time::Duration;
7
8use varnish_sys::ffi;
9use varnish_sys::vcl::{VclError, VclResult};
10
11/// The VSC (Varnish Shared Counter) is a way for outside programs to access Varnish statistics in a
12/// non-blocking way. The main way to access those counters traditionally is with `varnishstat`,
13/// but the API is generic and allows you to track, filter and read any counter that `varnishd`
14/// (and vmods) are exposing.
15///
16/// ```rust,no_run
17/// use varnish::MetricsReaderBuilder;
18/// fn main() {
19///
20///     let mut reader = MetricsReaderBuilder::new().build().unwrap_or_else(|e| {
21///         eprintln!("Error: {e}");
22///         std::process::exit(1);
23///     });
24///
25///     reader.update();
26///
27///     for stat in reader.stats().values() {
28///         println!("{}: {}", stat.name, stat.get_raw_value());
29///     }
30/// }
31/// ```
32#[derive(Debug)]
33pub struct MetricsReader<'a> {
34    vsm: *mut ffi::vsm,
35    vsc: *mut ffi::vsc,
36    internal: Box<MetricsReaderImpl<'a>>,
37}
38
39#[derive(Debug, Default)]
40struct MetricsReaderImpl<'a> {
41    points: HashMap<usize, Metric<'a>>,
42    added: Vec<usize>,
43    deleted: Vec<usize>,
44}
45
46/// Initialize and configure a [`MetricsReader`] but do not attach it to a running `varnishd` instance
47pub struct MetricsReaderBuilder<'a> {
48    vsm: *mut ffi::vsm,
49    vsc: *mut ffi::vsc,
50    phantom: PhantomData<&'a ()>,
51}
52
53impl<'a> MetricsReaderBuilder<'a> {
54    /// Create a new `VSCBuilder`
55    #[expect(clippy::new_without_default)] // TODO: consider implementing
56    pub fn new() -> Self {
57        unsafe {
58            let vsm = ffi::VSM_New();
59            assert!(!vsm.is_null());
60            let vsc = ffi::VSC_New();
61            assert!(!vsc.is_null());
62            // get raw value, we can always clamp them later
63            ffi::VSC_Arg(vsc, 'r' as c_char, ptr::null());
64            Self {
65                vsm,
66                vsc,
67                phantom: PhantomData,
68            }
69        }
70    }
71
72    /// Specify where to find the `varnishd` working directory.
73    ///
74    /// It's usually superfluous to call this function, unless `varnishd` itself was called with
75    /// the `-n` argument (in which case, both arguments should match)
76    pub fn work_dir(self, dir: &Path) -> Result<Self, NulError> {
77        let c_dir = CString::new(dir.to_str().expect("work_dir path must be valid UTF-8"))?;
78        let ret = unsafe { ffi::VSM_Arg(self.vsm, 'n' as c_char, c_dir.as_ptr()) };
79        assert_eq!(ret, 1);
80        Ok(self)
81    }
82
83    /// How long to wait when attaching
84    ///
85    /// When [`MetricsReaderBuilder::build()`] is called, it'll internally call `VSM_Attach`, hoping to find a running
86    /// `varnishd` instance. If `None`, the function will not return until it connects, otherwise
87    /// it specifies the timeout to use.
88    #[must_use]
89    pub fn patience(self, t: Option<Duration>) -> Self {
90        let arg = CString::new(match t {
91            None => "off".to_string(),
92            Some(t) => t.as_secs_f64().to_string(),
93        })
94        .expect("string constructed from f64 cannot contain null bytes");
95
96        // # Safety
97        // we just created this string, no point to double-check it for nul bytes
98        unsafe {
99            // TODO: document why this can fail, and if we should return an error
100            // TODO: Document why using `self.vsm` here, and `self.vsc` in the other `VSM_Arg` calls
101            let ret = ffi::VSM_Arg(self.vsm, 't' as c_char, arg.as_ptr());
102            assert_eq!(ret, 1);
103        }
104
105        self
106    }
107
108    fn vsc_arg(self, o: char, s: &str) -> Result<Self, NulError> {
109        let c_s = CString::new(s)?;
110        unsafe {
111            let ret = ffi::VSC_Arg(self.vsc, o as c_char, c_s.as_ptr());
112            assert_eq!(ret, 1);
113        }
114        Ok(self)
115    }
116
117    /// Provide a globbing pattern of statistics names to include.
118    ///
119    /// May be called multiple times, interleaved with [`MetricsReaderBuilder::exclude()`], the order matters.
120    pub fn include(self, s: &str) -> Result<Self, NulError> {
121        self.vsc_arg('I', s)
122    }
123
124    /// Provide a globbing pattern of statistics names to exclude.
125    ///
126    /// May be called multiple times, interleaved with [`MetricsReaderBuilder::include()`], the order matters.
127    pub fn exclude(self, s: &str) -> Result<Self, NulError> {
128        self.vsc_arg('X', s)
129    }
130
131    /// Provide a globbing pattern of statistics names to keep around, protecting them from
132    /// exclusion.
133    pub fn require(self, s: &str) -> Result<Self, NulError> {
134        self.vsc_arg('R', s)
135    }
136
137    /// Build the [`MetricsReader`], attaching to a running `varnishd` instance
138    pub fn build(mut self) -> VclResult<MetricsReader<'a>> {
139        let ret = unsafe { ffi::VSM_Attach(self.vsm, 0) };
140        if ret != 0 {
141            let err = vsm_error(self.vsm);
142            unsafe {
143                ffi::VSM_ResetError(self.vsm);
144            }
145            Err(err)
146        } else {
147            let mut internal = Box::new(MetricsReaderImpl::default());
148            unsafe {
149                ffi::VSC_State(
150                    self.vsc,
151                    Some(add_point),
152                    Some(del_point),
153                    ptr::from_mut::<MetricsReaderImpl>(&mut *internal).cast::<c_void>(),
154                );
155            }
156            let vsm = self.vsm;
157            let vsc = self.vsc;
158            // nullify so that .drop() doesn't destroy vsm/vsc
159            self.vsm = ptr::null_mut();
160            self.vsc = ptr::null_mut();
161            Ok(MetricsReader { vsm, vsc, internal })
162        }
163    }
164}
165
166fn vsm_error(p: *const ffi::vsm) -> VclError {
167    unsafe {
168        VclError::new(
169            CStr::from_ptr(ffi::VSM_Error(p))
170                .to_str()
171                .expect("VSM error message must be valid UTF-8")
172                .to_string(),
173        )
174    }
175}
176
177impl Drop for MetricsReaderBuilder<'_> {
178    fn drop(&mut self) {
179        assert!(
180            (self.vsc.is_null() && self.vsm.is_null())
181                || (!self.vsc.is_null() && !self.vsm.is_null())
182        );
183        if !self.vsc.is_null() {
184            unsafe {
185                ffi::VSC_Destroy(&raw mut self.vsc, self.vsm);
186            }
187        }
188    }
189}
190
191impl Drop for MetricsReader<'_> {
192    fn drop(&mut self) {
193        unsafe {
194            ffi::VSC_Destroy(&raw mut self.vsc, self.vsm);
195        }
196    }
197}
198
199/// Kind of data
200#[derive(Debug, Copy, Clone, Eq, PartialEq)]
201pub enum Semantics {
202    /// Value only goes up (e.g. amount of bytes transferred)
203    Counter,
204    /// Value can go up and down (e.g. amount of current connections)
205    Gauge,
206    /// Value is to be read as 64 booleans packed together as a `u64`
207    Bitmap,
208    /// No information on this value
209    Unknown,
210}
211
212impl From<c_int> for Semantics {
213    fn from(value: c_int) -> Self {
214        match char::from_u32(value as u32) {
215            Some('c') => Semantics::Counter,
216            Some('g') => Semantics::Gauge,
217            Some('b') => Semantics::Bitmap,
218            _ => Semantics::Unknown,
219        }
220    }
221}
222
223impl From<Semantics> for char {
224    fn from(value: Semantics) -> char {
225        match value {
226            Semantics::Counter => 'c',
227            Semantics::Gauge => 'g',
228            Semantics::Bitmap => 'b',
229            Semantics::Unknown => '?',
230        }
231    }
232}
233
234/// Unit of a value
235#[derive(Debug, Copy, Clone, Eq, PartialEq)]
236pub enum MetricFormat {
237    /// No unit
238    Integer,
239    /// Bytes, for data volumes
240    Bytes,
241    /// No unit, generally used with [`Semantics::Bitmap`]
242    Bitmap,
243    /// Time
244    Duration,
245    /// Unit unknown
246    // FIXME: This should contain original value
247    Unknown,
248}
249
250impl From<c_int> for MetricFormat {
251    fn from(value: c_int) -> Self {
252        match char::from_u32(value as u32) {
253            Some('i') => MetricFormat::Integer,
254            Some('B') => MetricFormat::Bytes,
255            Some('b') => MetricFormat::Bitmap,
256            Some('d') => MetricFormat::Duration,
257            _ => MetricFormat::Unknown,
258        }
259    }
260}
261
262impl From<MetricFormat> for char {
263    fn from(value: MetricFormat) -> char {
264        match value {
265            MetricFormat::Integer => 'i',
266            MetricFormat::Bytes => 'B',
267            MetricFormat::Bitmap => 'b',
268            MetricFormat::Duration => 'd',
269            MetricFormat::Unknown => '?',
270        }
271    }
272}
273
274unsafe extern "C" fn add_point(ptr: *mut c_void, point: *const ffi::VSC_point) -> *mut c_void {
275    // FIXME: handle errors without panic
276    let internal = ptr
277        .cast::<MetricsReaderImpl>()
278        .as_mut()
279        .expect("VSC add_point callback priv pointer must not be null");
280    let k = point as usize;
281    let point = point.as_ref().expect("VSC point pointer must not be null");
282
283    let stat = Metric {
284        value: point.ptr,
285        name: CStr::from_ptr(point.name)
286            .to_str()
287            .expect("VSC point name must be valid UTF-8"),
288        short_desc: CStr::from_ptr(point.sdesc)
289            .to_str()
290            .expect("VSC point short description must be valid UTF-8"),
291        long_desc: CStr::from_ptr(point.ldesc)
292            .to_str()
293            .expect("VSC point long description must be valid UTF-8"),
294        semantics: point.semantics.into(),
295        format: point.format.into(),
296    };
297    // FIXME: needs to be documented: pointer is used as a key?
298    assert_eq!(internal.points.insert(k, stat), None);
299    internal.added.push(k);
300    ptr::null_mut()
301}
302
303unsafe extern "C" fn del_point(ptr: *mut c_void, point: *const ffi::VSC_point) {
304    let internal = ptr
305        .cast::<MetricsReaderImpl>()
306        .as_mut()
307        .expect("VSC del_point callback priv pointer must not be null");
308    let k = point as usize;
309    assert!(internal.points.contains_key(&k));
310    internal.deleted.push(k);
311    assert!(internal.points.remove(&k).is_some());
312}
313
314/// A live statistic
315///
316/// Describes a live value, with little overhead over the C struct
317#[derive(Debug, Copy, Clone, Eq, PartialEq)]
318pub struct Metric<'a> {
319    value: *const u64,
320    pub name: &'a str,
321    pub short_desc: &'a str,
322    pub long_desc: &'a str,
323    pub semantics: Semantics,
324    pub format: MetricFormat,
325}
326
327impl Metric<'_> {
328    /// Retrieve the current value of the statistic, as-is
329    pub fn get_raw_value(&self) -> u64 {
330        // # Safety
331        // the pointer is valid as long as the VSC exist, and
332        // Stats::update() isn't called (which uses `&mut self`)
333        unsafe { *self.value }
334    }
335
336    /// Return a sanitized value of the statistic
337    ///
338    /// Gauges will fluctuate up and down, with multiple threads operating on them. As a result,
339    /// their value can go below 0 and underflow. This function will prevent the value from
340    /// wrapping around and just returns 0.
341    pub fn get_clamped_value(&self) -> u64 {
342        // # Safety
343        // the pointer is valid as long as VSC exist, and
344        // Stats::update() isn't called (which uses `&mut self`)
345        let v = unsafe { *self.value };
346        if i64::try_from(v).is_ok() {
347            v
348        } else {
349            0
350        }
351    }
352}
353
354impl MetricsReader<'_> {
355    /// Return a statistic set
356    ///
357    /// Names are not necessarily unique, so instead, statistics are tracked using `usize` handle
358    /// that can help you track which ones (dis)appeared during a [`MetricsReader::update()`] call.
359    ///
360    /// The C API guarantees we can access all the `Stat` in the `HashMap`, until the next `update`
361    /// call, so the `rust` API mirrors this here.
362    pub fn stats(&self) -> &HashMap<usize, Metric<'_>> {
363        &self.internal.points
364    }
365
366    /// Update the list of `Stat` we have access to
367    ///
368    /// You must call this function at least once to get access to any data (otherwise you'll just
369    /// get an empty `HashMap`).
370    ///
371    /// The two returned `Vec`s list the added and deleted keys in the `HashMap`, in case you need
372    /// to keep track of them at an individual level.
373    /// (if a key appears in both `Vec`s, the statistic got replaced).
374    pub fn update(&mut self) -> (Vec<usize>, Vec<usize>) {
375        unsafe {
376            ffi::VSC_Iter(self.vsc, self.vsm, None, ptr::null_mut());
377        }
378        let added = std::mem::take(&mut self.internal.added);
379        let deleted = std::mem::take(&mut self.internal.deleted);
380        (added, deleted)
381    }
382}