Skip to main content

kstat_rs/
lib.rs

1//! Rust library for interfacing with illumos kernel statistics, `libkstat`.
2//!
3//! The illumos `kstat` system is a kernel module for exporting data about the system to user
4//! processes. Users create a control handle to the system with [`Ctl::new`], which gives them
5//! access to the statistics exported by their system.
6//!
7//! Individual statistics are represented by the [`Kstat`] type, which includes information about
8//! the type of data, when it was created or last updated, and the actual data itself. The `Ctl`
9//! handle maintains a linked list of `Kstat` objects, which users may walk with the [`Ctl::iter`]
10//! method.
11//!
12//! Each kstat is identified by a module, an instance number, and a name. In addition, the data may
13//! be of several different types, such as name/value pairs or interrupt statistics. These types
14//! are captured by the [`Data`] enum, which can be read and returned by using the [`Ctl::read`]
15//! method.
16
17// Copyright 2023 Oxide Computer Company
18//
19// Licensed under the Apache License, Version 2.0 (the "License");
20// you may not use this file except in compliance with the License.
21// You may obtain a copy of the License at
22//
23//     http://www.apache.org/licenses/LICENSE-2.0
24//
25// Unless required by applicable law or agreed to in writing, software
26// distributed under the License is distributed on an "AS IS" BASIS,
27// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
28// See the License for the specific language governing permissions and
29// limitations under the License.
30
31use std::cmp::Ord;
32use std::cmp::Ordering;
33use std::cmp::PartialOrd;
34use std::convert::TryFrom;
35use std::marker::PhantomData;
36use thiserror::Error;
37
38mod sys;
39
40/// Kinds of errors returned by the library.
41#[derive(Debug, Error)]
42pub enum Error {
43    /// An attempt to convert a byte-string to a Rust string failed.
44    #[error("The byte-string is not a valid Rust string")]
45    InvalidString,
46
47    /// Encountered an invalid kstat type.
48    #[error("Kstat type {0} is invalid")]
49    InvalidType(u8),
50
51    /// Encountered an invalid named kstat data type.
52    #[error("The named kstat data type {0} is invalid")]
53    InvalidNamedType(u8),
54
55    /// Encountered a null pointer or empty data.
56    #[error("A null pointer or empty kstat was encountered")]
57    NullData,
58
59    /// Error bubbled up from operating on `libkstat`.
60    #[error(transparent)]
61    Io(#[from] std::io::Error),
62}
63
64/// `Ctl` is a handle to the kstat library.
65///
66/// Users instantiate a control handle and access the kstat's it contains, for example via the
67/// [`Ctl::iter`] method.
68#[derive(Debug)]
69pub struct Ctl {
70    ctl: *mut sys::kstat_ctl_t,
71}
72
73/// The `Ctl` wraps a raw pointer allocated by the `libkstat(3KSTAT)` library.
74/// This itself isn't thread-safe, but doesn't refer to any thread-local state.
75/// So it's safe to send across threads.
76unsafe impl Send for Ctl {}
77
78impl Ctl {
79    /// Create a new `Ctl`.
80    pub fn new() -> Result<Self, Error> {
81        sys::open().map(|ctl| Ctl { ctl })
82    }
83
84    /// Synchronize this `Ctl` with the kernel's view of the data.
85    ///
86    /// A `Ctl` is really a snapshot of the kernel's internal list of kstats. This method consumes
87    /// and updates a control object, bringing it into sync with the kernel's copy.
88    pub fn update(self) -> Result<Self, Error> {
89        sys::update(self.ctl).map(|_| self)
90    }
91
92    /// Return an iterator over the [`Kstat`]s in `self`.
93    ///
94    /// Note that this will only return `Kstat`s which are successfully read. For example, it will
95    /// ignore those with non-UTF-8 names.
96    pub fn iter(&self) -> Iter<'_> {
97        Iter {
98            kstat: unsafe { (*self.ctl).kc_chain },
99            _d: PhantomData,
100        }
101    }
102
103    /// Read a [`Kstat`], returning the data for it.
104    pub fn read<'a>(&self, kstat: &mut Kstat<'a>) -> Result<Data<'a>, Error> {
105        kstat.read(self.ctl)?;
106        kstat.data()
107    }
108
109    /// Find [`Kstat`]s by module, instance, and/or name.
110    ///
111    /// If a field is `None`, any matching `Kstat` is returned.
112    pub fn filter<'a>(
113        &'a self,
114        module: Option<&'a str>,
115        instance: Option<i32>,
116        name: Option<&'a str>,
117    ) -> impl Iterator<Item = Kstat<'a>> {
118        self.iter().filter(move |kstat| {
119            fn should_include<T>(inner: &T, cmp: &Option<T>) -> bool
120            where
121                T: PartialEq,
122            {
123                if let Some(cmp) = cmp {
124                    inner == cmp
125                } else {
126                    true // Include if this comparator is None
127                }
128            }
129            should_include(&kstat.ks_module, &module)
130                && should_include(&kstat.ks_instance, &instance)
131                && should_include(&kstat.ks_name, &name)
132        })
133    }
134}
135
136impl Drop for Ctl {
137    fn drop(&mut self) {
138        let _ = sys::close(self.ctl);
139    }
140}
141
142#[derive(Debug)]
143pub struct Iter<'a> {
144    kstat: *mut sys::kstat_t,
145    _d: PhantomData<&'a ()>,
146}
147
148impl<'a> Iterator for Iter<'a> {
149    type Item = Kstat<'a>;
150
151    fn next(&mut self) -> Option<Self::Item> {
152        loop {
153            if let Some(ks) = unsafe { self.kstat.as_ref() } {
154                self.kstat = unsafe { *self.kstat }.ks_next;
155                if let Ok(ks) = Kstat::try_from(ks) {
156                    break Some(ks);
157                }
158                // continue to next kstat
159            } else {
160                break None;
161            }
162        }
163    }
164}
165
166unsafe impl<'a> Send for Iter<'a> {}
167
168/// `Kstat` represents a single kernel statistic.
169#[derive(Clone, Copy, Debug, Eq, PartialEq)]
170pub struct Kstat<'a> {
171    /// The creation time of the stat, in nanoseconds.
172    pub ks_crtime: i64,
173    /// The time of the last update, in nanoseconds.
174    pub ks_snaptime: i64,
175    /// The module of the kstat.
176    pub ks_module: &'a str,
177    /// The instance of the kstat.
178    pub ks_instance: i32,
179    /// The name of the kstat.
180    pub ks_name: &'a str,
181    /// The type of the kstat.
182    pub ks_type: Type,
183    /// The class of the kstat.
184    pub ks_class: &'a str,
185    ks: *mut sys::kstat_t,
186}
187
188#[allow(clippy::non_canonical_partial_ord_impl)]
189impl<'a> PartialOrd for Kstat<'a> {
190    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
191        Some(
192            self.ks_class
193                .cmp(other.ks_class)
194                .then_with(|| self.ks_module.cmp(other.ks_module))
195                .then_with(|| self.ks_instance.cmp(&other.ks_instance))
196                .then_with(|| self.ks_name.cmp(other.ks_name))
197                .then_with(|| self.ks_class.cmp(other.ks_name)),
198        )
199    }
200}
201
202impl<'a> Ord for Kstat<'a> {
203    fn cmp(&self, other: &Self) -> Ordering {
204        self.partial_cmp(other).unwrap()
205    }
206}
207
208unsafe impl<'a> Send for Kstat<'a> {}
209
210impl<'a> Kstat<'a> {
211    /// Construct a `Kstat` without an underlying `kstat_t`. Used for testing
212    /// purposes only.
213    pub fn with_null_kstat(ks_module: &'a str, ks_instance: i32, ks_name: &'a str) -> Self {
214        Self {
215            ks_crtime: 0,
216            ks_snaptime: 0,
217            ks_module,
218            ks_instance,
219            ks_name,
220            ks_type: Type::Named,
221            ks_class: "",
222            ks: std::ptr::null_mut(),
223        }
224    }
225
226    fn read(&mut self, ctl: *mut sys::kstat_ctl_t) -> Result<(), Error> {
227        sys::read(ctl, self.ks, std::ptr::null_mut())?;
228        self.ks_snaptime = unsafe { (*self.ks).ks_snaptime };
229        Ok(())
230    }
231
232    fn data(&self) -> Result<Data<'a>, Error> {
233        let ks = unsafe { self.ks.as_ref() }.ok_or_else(|| Error::NullData)?;
234        match self.ks_type {
235            Type::Raw => Ok(Data::Raw(sys::kstat_data_raw(ks))),
236            Type::Named => Ok(Data::Named(
237                sys::kstat_data_named(ks)
238                    .iter()
239                    .map(Named::try_from)
240                    .collect::<Result<_, _>>()?,
241            )),
242            Type::Intr => Ok(Data::Intr(Intr::from(sys::kstat_data_intr(ks)))),
243            Type::Io => Ok(Data::Io(Io::from(sys::kstat_data_io(ks)))),
244            Type::Timer => Ok(Data::Timer(
245                sys::kstat_data_timer(ks)
246                    .iter()
247                    .map(Timer::try_from)
248                    .collect::<Result<_, _>>()?,
249            )),
250        }
251    }
252}
253
254impl<'a> TryFrom<&'a sys::kstat_t> for Kstat<'a> {
255    type Error = Error;
256    fn try_from(k: &'a sys::kstat_t) -> Result<Self, Self::Error> {
257        Ok(Kstat {
258            ks_crtime: k.ks_crtime,
259            ks_snaptime: k.ks_snaptime,
260            ks_module: sys::array_to_cstr(&k.ks_module)?,
261            ks_instance: k.ks_instance,
262            ks_name: sys::array_to_cstr(&k.ks_name)?,
263            ks_type: Type::try_from(k.ks_type)?,
264            ks_class: sys::array_to_cstr(&k.ks_name)?,
265            ks: k as *const _ as *mut _,
266        })
267    }
268}
269
270impl<'a> TryFrom<&'a *mut sys::kstat_t> for Kstat<'a> {
271    type Error = Error;
272    fn try_from(k: &'a *mut sys::kstat_t) -> Result<Self, Self::Error> {
273        if let Some(k) = unsafe { k.as_ref() } {
274            Kstat::try_from(k)
275        } else {
276            Err(Error::NullData)
277        }
278    }
279}
280
281/// The type of a kstat.
282#[derive(Debug, Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
283pub enum Type {
284    Raw,
285    Named,
286    Intr,
287    Io,
288    Timer,
289}
290
291impl TryFrom<u8> for Type {
292    type Error = Error;
293    fn try_from(t: u8) -> Result<Self, Self::Error> {
294        match t {
295            sys::KSTAT_TYPE_RAW => Ok(Type::Raw),
296            sys::KSTAT_TYPE_NAMED => Ok(Type::Named),
297            sys::KSTAT_TYPE_INTR => Ok(Type::Intr),
298            sys::KSTAT_TYPE_IO => Ok(Type::Io),
299            sys::KSTAT_TYPE_TIMER => Ok(Type::Timer),
300            other => Err(Self::Error::InvalidType(other)),
301        }
302    }
303}
304
305/// The data type of a single name/value pair of a named kstat.
306#[derive(Debug, Copy, Clone, PartialEq)]
307pub enum NamedType {
308    Char,
309    Int32,
310    UInt32,
311    Int64,
312    UInt64,
313    String,
314}
315
316impl TryFrom<u8> for NamedType {
317    type Error = Error;
318    fn try_from(t: u8) -> Result<Self, Self::Error> {
319        match t {
320            sys::KSTAT_DATA_CHAR => Ok(NamedType::Char),
321            sys::KSTAT_DATA_INT32 => Ok(NamedType::Int32),
322            sys::KSTAT_DATA_UINT32 => Ok(NamedType::UInt32),
323            sys::KSTAT_DATA_INT64 => Ok(NamedType::Int64),
324            sys::KSTAT_DATA_UINT64 => Ok(NamedType::UInt64),
325            sys::KSTAT_DATA_STRING => Ok(NamedType::String),
326            other => Err(Self::Error::InvalidNamedType(other)),
327        }
328    }
329}
330
331/// Data from a single kstat.
332#[derive(Clone, Debug)]
333pub enum Data<'a> {
334    Raw(Vec<&'a [u8]>),
335    Named(Vec<Named<'a>>),
336    Intr(Intr),
337    Io(Io),
338    Timer(Vec<Timer<'a>>),
339    Null,
340}
341
342/// An I/O kernel statistic
343#[derive(Debug, Clone, Copy)]
344pub struct Io {
345    pub nread: u64,
346    pub nwritten: u64,
347    pub reads: u32,
348    pub writes: u32,
349    pub wtime: i64,
350    pub wlentime: i64,
351    pub wlastupdate: i64,
352    pub rtime: i64,
353    pub rlentime: i64,
354    pub rlastupdate: i64,
355    pub wcnt: u32,
356    pub rcnt: u32,
357}
358
359impl From<&sys::kstat_io_t> for Io {
360    fn from(k: &sys::kstat_io_t) -> Self {
361        Io {
362            nread: k.nread,
363            nwritten: k.nwritten,
364            reads: k.reads,
365            writes: k.writes,
366            wtime: k.wtime,
367            wlentime: k.wlentime,
368            wlastupdate: k.wlastupdate,
369            rtime: k.rtime,
370            rlentime: k.rlentime,
371            rlastupdate: k.rlastupdate,
372            wcnt: k.wcnt,
373            rcnt: k.rcnt,
374        }
375    }
376}
377
378impl TryFrom<&*const sys::kstat_io_t> for Io {
379    type Error = Error;
380    fn try_from(k: &*const sys::kstat_io_t) -> Result<Self, Self::Error> {
381        if let Some(k) = unsafe { k.as_ref() } {
382            Ok(Io::from(k))
383        } else {
384            Err(Error::NullData)
385        }
386    }
387}
388
389/// A timer kernel statistic.
390#[derive(Debug, Copy, Clone)]
391pub struct Timer<'a> {
392    pub name: &'a str,
393    pub num_events: usize,
394    pub elapsed_time: i64,
395    pub min_time: i64,
396    pub max_time: i64,
397    pub start_time: i64,
398    pub stop_time: i64,
399}
400
401impl<'a> TryFrom<&'a sys::kstat_timer_t> for Timer<'a> {
402    type Error = Error;
403    fn try_from(k: &'a sys::kstat_timer_t) -> Result<Self, Self::Error> {
404        Ok(Self {
405            name: sys::array_to_cstr(&k.name)?,
406            num_events: k.num_events as _,
407            elapsed_time: k.elapsed_time,
408            min_time: k.min_time,
409            max_time: k.max_time,
410            start_time: k.start_time,
411            stop_time: k.stop_time,
412        })
413    }
414}
415
416impl<'a> TryFrom<&'a *const sys::kstat_timer_t> for Timer<'a> {
417    type Error = Error;
418    fn try_from(k: &'a *const sys::kstat_timer_t) -> Result<Self, Self::Error> {
419        if let Some(k) = unsafe { k.as_ref() } {
420            Timer::try_from(k)
421        } else {
422            Err(Error::NullData)
423        }
424    }
425}
426
427/// Interrupt kernel statistic.
428#[derive(Debug, Copy, Clone)]
429pub struct Intr {
430    pub hard: u32,
431    pub soft: u32,
432    pub watchdog: u32,
433    pub spurious: u32,
434    pub multisvc: u32,
435}
436
437impl From<&sys::kstat_intr_t> for Intr {
438    fn from(k: &sys::kstat_intr_t) -> Self {
439        Self {
440            hard: k.intr_hard,
441            soft: k.intr_soft,
442            watchdog: k.intr_watchdog,
443            spurious: k.intr_spurious,
444            multisvc: k.intr_multisvc,
445        }
446    }
447}
448
449impl TryFrom<&*const sys::kstat_intr_t> for Intr {
450    type Error = Error;
451    fn try_from(k: &*const sys::kstat_intr_t) -> Result<Self, Self::Error> {
452        if let Some(k) = unsafe { k.as_ref() } {
453            Ok(Intr::from(k))
454        } else {
455            Err(Error::NullData)
456        }
457    }
458}
459
460/// A name/value data element from a named kernel statistic.
461#[derive(Clone, Debug)]
462pub struct Named<'a> {
463    pub name: &'a str,
464    pub value: NamedData<'a>,
465}
466
467impl<'a> Named<'a> {
468    /// Return the data type of a named kernel statistic.
469    pub fn data_type(&self) -> NamedType {
470        self.value.data_type()
471    }
472}
473
474/// The value part of a name-value kernel statistic.
475#[derive(Clone, Debug)]
476pub enum NamedData<'a> {
477    Char(&'a [u8]),
478    Int32(i32),
479    UInt32(u32),
480    Int64(i64),
481    UInt64(u64),
482    String(&'a str),
483}
484
485impl<'a> NamedData<'a> {
486    /// Return the data type of a named kernel statistic.
487    pub fn data_type(&self) -> NamedType {
488        match self {
489            NamedData::Char(_) => NamedType::Char,
490            NamedData::Int32(_) => NamedType::Int32,
491            NamedData::UInt32(_) => NamedType::UInt32,
492            NamedData::Int64(_) => NamedType::Int64,
493            NamedData::UInt64(_) => NamedType::UInt64,
494            NamedData::String(_) => NamedType::String,
495        }
496    }
497}
498
499impl<'a> TryFrom<&'a sys::kstat_named_t> for Named<'a> {
500    type Error = Error;
501    fn try_from(k: &'a sys::kstat_named_t) -> Result<Self, Self::Error> {
502        let name = sys::array_to_cstr(&k.name)?;
503        match NamedType::try_from(k.data_type)? {
504            NamedType::Char => {
505                let slice = unsafe {
506                    let p = k.value.charc.as_ptr();
507                    let len = k.value.charc.len();
508                    std::slice::from_raw_parts(p, len)
509                };
510                Ok(Named {
511                    name,
512                    value: NamedData::Char(slice),
513                })
514            }
515            NamedType::Int32 => Ok(Named {
516                name,
517                value: NamedData::Int32(unsafe { k.value.i32 }),
518            }),
519            NamedType::UInt32 => Ok(Named {
520                name,
521                value: NamedData::UInt32(unsafe { k.value.ui32 }),
522            }),
523            NamedType::Int64 => Ok(Named {
524                name,
525                value: NamedData::Int64(unsafe { k.value.i64 }),
526            }),
527
528            NamedType::UInt64 => Ok(Named {
529                name,
530                value: NamedData::UInt64(unsafe { k.value.ui64 }),
531            }),
532            NamedType::String => {
533                let s = (&unsafe { k.value.str }).try_into()?;
534                Ok(Named {
535                    name,
536                    value: NamedData::String(s),
537                })
538            }
539        }
540    }
541}
542
543#[cfg(all(test, target_os = "illumos"))]
544mod test {
545    use super::*;
546    use std::collections::BTreeMap;
547
548    #[test]
549    fn basic_test() {
550        let ctl = Ctl::new().expect("Failed to create kstat control");
551        for mut kstat in ctl.iter() {
552            match ctl.read(&mut kstat) {
553                Ok(_) => {}
554                Err(e) => {
555                    println!("{}", e);
556                }
557            }
558        }
559    }
560
561    #[test]
562    fn compare_with_kstat_cli() {
563        let ctl = Ctl::new().expect("Failed to create kstat control");
564        let mut kstat = ctl
565            .filter(Some("cpu_info"), Some(0), Some("cpu_info0"))
566            .next()
567            .expect("Failed to find kstat cpu_info:0:cpu_info0");
568        if let Data::Named(data) = ctl.read(&mut kstat).expect("Failed to read kstat") {
569            let mut items = BTreeMap::new();
570            for item in data.iter() {
571                items.insert(item.name, item);
572            }
573            let out = subprocess::Exec::cmd("/usr/bin/kstat")
574                .arg("-p")
575                .arg("cpu_info:0:cpu_info0:")
576                .stdout(subprocess::Redirection::Pipe)
577                .capture()
578                .expect("Failed to run /usr/bin/kstat");
579            let kstat_items: BTreeMap<_, _> = String::from_utf8(out.stdout)
580                .expect("Non UTF-8 output from kstat")
581                .lines()
582                .filter_map(|line| {
583                    let parts = line.trim().split('\t').collect::<Vec<_>>();
584                    assert_eq!(
585                        parts.len(),
586                        2,
587                        "Lines from kstat should be 2 tab-separated items, found {:#?}",
588                        parts
589                    );
590                    let (id, value) = (parts[0], parts[1]);
591                    if id.ends_with("crtime") {
592                        let crtime: f64 = value.parse().expect("Expected a crtime in nanoseconds");
593                        let crtime = (crtime * 1e9) as i64;
594                        assert!(
595                            (crtime - kstat.ks_crtime) < 5 || (kstat.ks_crtime - crtime) < 5,
596                            "Expected nearly equal crtimes"
597                        );
598                        // Don't push this value
599                        None
600                    } else if id.ends_with("snaptime") {
601                        let snaptime: f64 =
602                            value.parse().expect("Expected a snaptime in nanoseconds");
603                        let snaptime = (snaptime * 1e9) as i64;
604                        assert!(
605                            (snaptime - kstat.ks_snaptime) < 5
606                                || (kstat.ks_snaptime - snaptime) < 5,
607                            "Expected nearly equal snaptimes"
608                        );
609                        // Don't push this value
610                        None
611                    } else if id.ends_with("class") {
612                        // Don't push this value
613                        None
614                    } else {
615                        Some((id.to_string(), value.to_string()))
616                    }
617                })
618                .collect();
619            assert_eq!(
620                items.len(),
621                kstat_items.len(),
622                "Expected the same number of items from /usr/bin/kstat:\n{:#?}\n{:#?}",
623                items,
624                kstat_items
625            );
626            const SKIPPED_STATS: &[&'static str] = &["current_clock_Hz", "current_cstate"];
627            for (key, value) in kstat_items.iter() {
628                let name = key.split(':').last().expect("Expected to split on ':'");
629                if SKIPPED_STATS.contains(&name) {
630                    println!("Skipping stat '{}', not stable enough for testing", name);
631                    continue;
632                }
633                let item = items
634                    .get(name)
635                    .expect(&format!("Expected a name/value pair with name '{}'", name));
636                println!("key: {:#?}\nvalue: {:#?}", key, value);
637                println!("item: {:#?}", item);
638                match item.value {
639                    NamedData::Char(slice) => {
640                        for (sl, by) in slice.iter().zip(value.as_bytes().iter()) {
641                            if by == &0 {
642                                break;
643                            }
644                            assert_eq!(sl, by, "Expected equal bytes, found {} and {}", sl, by);
645                        }
646                    }
647                    NamedData::Int32(i) => assert_eq!(i, value.parse().unwrap()),
648                    NamedData::UInt32(u) => assert_eq!(u, value.parse().unwrap()),
649                    NamedData::Int64(i) => assert_eq!(i, value.parse().unwrap()),
650                    NamedData::UInt64(u) => assert_eq!(u, value.parse().unwrap()),
651                    NamedData::String(s) => assert_eq!(s, value),
652                }
653            }
654        }
655    }
656}