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    fn read(&mut self, ctl: *mut sys::kstat_ctl_t) -> Result<(), Error> {
212        sys::read(ctl, self.ks, std::ptr::null_mut())?;
213        self.ks_snaptime = unsafe { (*self.ks).ks_snaptime };
214        Ok(())
215    }
216
217    fn data(&self) -> Result<Data<'a>, Error> {
218        let ks = unsafe { self.ks.as_ref() }.ok_or_else(|| Error::NullData)?;
219        match self.ks_type {
220            Type::Raw => Ok(Data::Raw(sys::kstat_data_raw(ks))),
221            Type::Named => Ok(Data::Named(
222                sys::kstat_data_named(ks)
223                    .iter()
224                    .map(Named::try_from)
225                    .collect::<Result<_, _>>()?,
226            )),
227            Type::Intr => Ok(Data::Intr(Intr::from(sys::kstat_data_intr(ks)))),
228            Type::Io => Ok(Data::Io(Io::from(sys::kstat_data_io(ks)))),
229            Type::Timer => Ok(Data::Timer(
230                sys::kstat_data_timer(ks)
231                    .iter()
232                    .map(Timer::try_from)
233                    .collect::<Result<_, _>>()?,
234            )),
235        }
236    }
237}
238
239impl<'a> TryFrom<&'a sys::kstat_t> for Kstat<'a> {
240    type Error = Error;
241    fn try_from(k: &'a sys::kstat_t) -> Result<Self, Self::Error> {
242        Ok(Kstat {
243            ks_crtime: k.ks_crtime,
244            ks_snaptime: k.ks_snaptime,
245            ks_module: sys::array_to_cstr(&k.ks_module)?,
246            ks_instance: k.ks_instance,
247            ks_name: sys::array_to_cstr(&k.ks_name)?,
248            ks_type: Type::try_from(k.ks_type)?,
249            ks_class: sys::array_to_cstr(&k.ks_name)?,
250            ks: k as *const _ as *mut _,
251        })
252    }
253}
254
255impl<'a> TryFrom<&'a *mut sys::kstat_t> for Kstat<'a> {
256    type Error = Error;
257    fn try_from(k: &'a *mut sys::kstat_t) -> Result<Self, Self::Error> {
258        if let Some(k) = unsafe { k.as_ref() } {
259            Kstat::try_from(k)
260        } else {
261            Err(Error::NullData)
262        }
263    }
264}
265
266/// The type of a kstat.
267#[derive(Debug, Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
268pub enum Type {
269    Raw,
270    Named,
271    Intr,
272    Io,
273    Timer,
274}
275
276impl TryFrom<u8> for Type {
277    type Error = Error;
278    fn try_from(t: u8) -> Result<Self, Self::Error> {
279        match t {
280            sys::KSTAT_TYPE_RAW => Ok(Type::Raw),
281            sys::KSTAT_TYPE_NAMED => Ok(Type::Named),
282            sys::KSTAT_TYPE_INTR => Ok(Type::Intr),
283            sys::KSTAT_TYPE_IO => Ok(Type::Io),
284            sys::KSTAT_TYPE_TIMER => Ok(Type::Timer),
285            other => Err(Self::Error::InvalidType(other)),
286        }
287    }
288}
289
290/// The data type of a single name/value pair of a named kstat.
291#[derive(Debug, Copy, Clone, PartialEq)]
292pub enum NamedType {
293    Char,
294    Int32,
295    UInt32,
296    Int64,
297    UInt64,
298    String,
299}
300
301impl TryFrom<u8> for NamedType {
302    type Error = Error;
303    fn try_from(t: u8) -> Result<Self, Self::Error> {
304        match t {
305            sys::KSTAT_DATA_CHAR => Ok(NamedType::Char),
306            sys::KSTAT_DATA_INT32 => Ok(NamedType::Int32),
307            sys::KSTAT_DATA_UINT32 => Ok(NamedType::UInt32),
308            sys::KSTAT_DATA_INT64 => Ok(NamedType::Int64),
309            sys::KSTAT_DATA_UINT64 => Ok(NamedType::UInt64),
310            sys::KSTAT_DATA_STRING => Ok(NamedType::String),
311            other => Err(Self::Error::InvalidNamedType(other)),
312        }
313    }
314}
315
316/// Data from a single kstat.
317#[derive(Clone, Debug)]
318pub enum Data<'a> {
319    Raw(Vec<&'a [u8]>),
320    Named(Vec<Named<'a>>),
321    Intr(Intr),
322    Io(Io),
323    Timer(Vec<Timer<'a>>),
324    Null,
325}
326
327/// An I/O kernel statistic
328#[derive(Debug, Clone, Copy)]
329pub struct Io {
330    pub nread: u64,
331    pub nwritten: u64,
332    pub reads: u32,
333    pub writes: u32,
334    pub wtime: i64,
335    pub wlentime: i64,
336    pub wlastupdate: i64,
337    pub rtime: i64,
338    pub rlentime: i64,
339    pub rlastupdate: i64,
340    pub wcnt: u32,
341    pub rcnt: u32,
342}
343
344impl From<&sys::kstat_io_t> for Io {
345    fn from(k: &sys::kstat_io_t) -> Self {
346        Io {
347            nread: k.nread,
348            nwritten: k.nwritten,
349            reads: k.reads,
350            writes: k.writes,
351            wtime: k.wtime,
352            wlentime: k.wlentime,
353            wlastupdate: k.wlastupdate,
354            rtime: k.rtime,
355            rlentime: k.rlentime,
356            rlastupdate: k.rlastupdate,
357            wcnt: k.wcnt,
358            rcnt: k.rcnt,
359        }
360    }
361}
362
363impl TryFrom<&*const sys::kstat_io_t> for Io {
364    type Error = Error;
365    fn try_from(k: &*const sys::kstat_io_t) -> Result<Self, Self::Error> {
366        if let Some(k) = unsafe { k.as_ref() } {
367            Ok(Io::from(k))
368        } else {
369            Err(Error::NullData)
370        }
371    }
372}
373
374/// A timer kernel statistic.
375#[derive(Debug, Copy, Clone)]
376pub struct Timer<'a> {
377    pub name: &'a str,
378    pub num_events: usize,
379    pub elapsed_time: i64,
380    pub min_time: i64,
381    pub max_time: i64,
382    pub start_time: i64,
383    pub stop_time: i64,
384}
385
386impl<'a> TryFrom<&'a sys::kstat_timer_t> for Timer<'a> {
387    type Error = Error;
388    fn try_from(k: &'a sys::kstat_timer_t) -> Result<Self, Self::Error> {
389        Ok(Self {
390            name: sys::array_to_cstr(&k.name)?,
391            num_events: k.num_events as _,
392            elapsed_time: k.elapsed_time,
393            min_time: k.min_time,
394            max_time: k.max_time,
395            start_time: k.start_time,
396            stop_time: k.stop_time,
397        })
398    }
399}
400
401impl<'a> TryFrom<&'a *const sys::kstat_timer_t> for Timer<'a> {
402    type Error = Error;
403    fn try_from(k: &'a *const sys::kstat_timer_t) -> Result<Self, Self::Error> {
404        if let Some(k) = unsafe { k.as_ref() } {
405            Timer::try_from(k)
406        } else {
407            Err(Error::NullData)
408        }
409    }
410}
411
412/// Interrupt kernel statistic.
413#[derive(Debug, Copy, Clone)]
414pub struct Intr {
415    pub hard: u32,
416    pub soft: u32,
417    pub watchdog: u32,
418    pub spurious: u32,
419    pub multisvc: u32,
420}
421
422impl From<&sys::kstat_intr_t> for Intr {
423    fn from(k: &sys::kstat_intr_t) -> Self {
424        Self {
425            hard: k.intr_hard,
426            soft: k.intr_soft,
427            watchdog: k.intr_watchdog,
428            spurious: k.intr_spurious,
429            multisvc: k.intr_multisvc,
430        }
431    }
432}
433
434impl TryFrom<&*const sys::kstat_intr_t> for Intr {
435    type Error = Error;
436    fn try_from(k: &*const sys::kstat_intr_t) -> Result<Self, Self::Error> {
437        if let Some(k) = unsafe { k.as_ref() } {
438            Ok(Intr::from(k))
439        } else {
440            Err(Error::NullData)
441        }
442    }
443}
444
445/// A name/value data element from a named kernel statistic.
446#[derive(Clone, Debug)]
447pub struct Named<'a> {
448    pub name: &'a str,
449    pub value: NamedData<'a>,
450}
451
452impl<'a> Named<'a> {
453    /// Return the data type of a named kernel statistic.
454    pub fn data_type(&self) -> NamedType {
455        self.value.data_type()
456    }
457}
458
459/// The value part of a name-value kernel statistic.
460#[derive(Clone, Debug)]
461pub enum NamedData<'a> {
462    Char(&'a [u8]),
463    Int32(i32),
464    UInt32(u32),
465    Int64(i64),
466    UInt64(u64),
467    String(&'a str),
468}
469
470impl<'a> NamedData<'a> {
471    /// Return the data type of a named kernel statistic.
472    pub fn data_type(&self) -> NamedType {
473        match self {
474            NamedData::Char(_) => NamedType::Char,
475            NamedData::Int32(_) => NamedType::Int32,
476            NamedData::UInt32(_) => NamedType::UInt32,
477            NamedData::Int64(_) => NamedType::Int64,
478            NamedData::UInt64(_) => NamedType::UInt64,
479            NamedData::String(_) => NamedType::String,
480        }
481    }
482}
483
484impl<'a> TryFrom<&'a sys::kstat_named_t> for Named<'a> {
485    type Error = Error;
486    fn try_from(k: &'a sys::kstat_named_t) -> Result<Self, Self::Error> {
487        let name = sys::array_to_cstr(&k.name)?;
488        match NamedType::try_from(k.data_type)? {
489            NamedType::Char => {
490                let slice = unsafe {
491                    let p = k.value.charc.as_ptr();
492                    let len = k.value.charc.len();
493                    std::slice::from_raw_parts(p, len)
494                };
495                Ok(Named {
496                    name,
497                    value: NamedData::Char(slice),
498                })
499            }
500            NamedType::Int32 => Ok(Named {
501                name,
502                value: NamedData::Int32(unsafe { k.value.i32 }),
503            }),
504            NamedType::UInt32 => Ok(Named {
505                name,
506                value: NamedData::UInt32(unsafe { k.value.ui32 }),
507            }),
508            NamedType::Int64 => Ok(Named {
509                name,
510                value: NamedData::Int64(unsafe { k.value.i64 }),
511            }),
512
513            NamedType::UInt64 => Ok(Named {
514                name,
515                value: NamedData::UInt64(unsafe { k.value.ui64 }),
516            }),
517            NamedType::String => {
518                let s = (&unsafe { k.value.str }).try_into()?;
519                Ok(Named {
520                    name,
521                    value: NamedData::String(s),
522                })
523            }
524        }
525    }
526}
527
528#[cfg(all(test, target_os = "illumos"))]
529mod test {
530    use super::*;
531    use std::collections::BTreeMap;
532
533    #[test]
534    fn basic_test() {
535        let ctl = Ctl::new().expect("Failed to create kstat control");
536        for mut kstat in ctl.iter() {
537            match ctl.read(&mut kstat) {
538                Ok(_) => {}
539                Err(e) => {
540                    println!("{}", e);
541                }
542            }
543        }
544    }
545
546    #[test]
547    fn compare_with_kstat_cli() {
548        let ctl = Ctl::new().expect("Failed to create kstat control");
549        let mut kstat = ctl
550            .filter(Some("cpu_info"), Some(0), Some("cpu_info0"))
551            .next()
552            .expect("Failed to find kstat cpu_info:0:cpu_info0");
553        if let Data::Named(data) = ctl.read(&mut kstat).expect("Failed to read kstat") {
554            let mut items = BTreeMap::new();
555            for item in data.iter() {
556                items.insert(item.name, item);
557            }
558            let out = subprocess::Exec::cmd("/usr/bin/kstat")
559                .arg("-p")
560                .arg("cpu_info:0:cpu_info0:")
561                .stdout(subprocess::Redirection::Pipe)
562                .capture()
563                .expect("Failed to run /usr/bin/kstat");
564            let kstat_items: BTreeMap<_, _> = String::from_utf8(out.stdout)
565                .expect("Non UTF-8 output from kstat")
566                .lines()
567                .filter_map(|line| {
568                    let parts = line.trim().split('\t').collect::<Vec<_>>();
569                    assert_eq!(
570                        parts.len(),
571                        2,
572                        "Lines from kstat should be 2 tab-separated items, found {:#?}",
573                        parts
574                    );
575                    let (id, value) = (parts[0], parts[1]);
576                    if id.ends_with("crtime") {
577                        let crtime: f64 = value.parse().expect("Expected a crtime in nanoseconds");
578                        let crtime = (crtime * 1e9) as i64;
579                        assert!(
580                            (crtime - kstat.ks_crtime) < 5 || (kstat.ks_crtime - crtime) < 5,
581                            "Expected nearly equal crtimes"
582                        );
583                        // Don't push this value
584                        None
585                    } else if id.ends_with("snaptime") {
586                        let snaptime: f64 =
587                            value.parse().expect("Expected a snaptime in nanoseconds");
588                        let snaptime = (snaptime * 1e9) as i64;
589                        assert!(
590                            (snaptime - kstat.ks_snaptime) < 5
591                                || (kstat.ks_snaptime - snaptime) < 5,
592                            "Expected nearly equal snaptimes"
593                        );
594                        // Don't push this value
595                        None
596                    } else if id.ends_with("class") {
597                        // Don't push this value
598                        None
599                    } else {
600                        Some((id.to_string(), value.to_string()))
601                    }
602                })
603                .collect();
604            assert_eq!(
605                items.len(),
606                kstat_items.len(),
607                "Expected the same number of items from /usr/bin/kstat:\n{:#?}\n{:#?}",
608                items,
609                kstat_items
610            );
611            const SKIPPED_STATS: &[&'static str] = &["current_clock_Hz", "current_cstate"];
612            for (key, value) in kstat_items.iter() {
613                let name = key.split(':').last().expect("Expected to split on ':'");
614                if SKIPPED_STATS.contains(&name) {
615                    println!("Skipping stat '{}', not stable enough for testing", name);
616                    continue;
617                }
618                let item = items
619                    .get(name)
620                    .expect(&format!("Expected a name/value pair with name '{}'", name));
621                println!("key: {:#?}\nvalue: {:#?}", key, value);
622                println!("item: {:#?}", item);
623                match item.value {
624                    NamedData::Char(slice) => {
625                        for (sl, by) in slice.iter().zip(value.as_bytes().iter()) {
626                            if by == &0 {
627                                break;
628                            }
629                            assert_eq!(sl, by, "Expected equal bytes, found {} and {}", sl, by);
630                        }
631                    }
632                    NamedData::Int32(i) => assert_eq!(i, value.parse().unwrap()),
633                    NamedData::UInt32(u) => assert_eq!(u, value.parse().unwrap()),
634                    NamedData::Int64(i) => assert_eq!(i, value.parse().unwrap()),
635                    NamedData::UInt64(u) => assert_eq!(u, value.parse().unwrap()),
636                    NamedData::String(s) => assert_eq!(s, value),
637                }
638            }
639        }
640    }
641}