├── .gitignore ├── .semaphore └── semaphore.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── Vagrantfile ├── docker ├── centos_8 │ └── Dockerfile ├── fedora_31 │ └── Dockerfile ├── ubuntu_1404 │ └── Dockerfile ├── ubuntu_1604 │ └── Dockerfile ├── ubuntu_1804 │ └── Dockerfile ├── ubuntu_2004 │ └── Dockerfile └── ubuntu_2204 │ └── Dockerfile ├── fixtures └── linux │ ├── cpu │ ├── proc_stat │ ├── proc_stat_1 │ ├── proc_stat_2 │ ├── proc_stat_garbage │ ├── proc_stat_incomplete │ └── proc_stat_partial │ ├── disk_stats │ ├── proc_diskstats │ ├── proc_diskstats_4_18 │ ├── proc_diskstats_5_5 │ ├── proc_diskstats_garbage │ └── proc_diskstats_incomplete │ ├── disk_usage │ ├── df │ ├── df_garbage │ ├── df_i │ ├── df_i_dash_percentage │ └── df_incomplete │ ├── load │ ├── proc_loadavg │ ├── proc_loadavg_garbage │ └── proc_loadavg_incomplete │ ├── memory │ ├── proc_meminfo │ ├── proc_meminfo_garbage │ └── proc_meminfo_incomplete │ ├── network │ ├── proc_net_dev │ ├── proc_net_dev_garbage │ └── proc_net_dev_incomplete │ ├── proc │ └── self │ │ └── cgroup │ │ ├── docker │ │ ├── docker_systemd │ │ ├── kubernetes │ │ ├── lxc │ │ └── none │ ├── process_memory │ ├── proc_self_statm │ ├── proc_self_statm_garbage │ └── proc_self_statm_incomplete │ └── sys │ └── fs │ ├── cgroup_v1 │ ├── cpu_quota │ │ ├── cpu.cfs_period_us │ │ ├── cpu.cfs_quota_us.half_cpu │ │ ├── cpu.cfs_quota_us.minus_one │ │ ├── cpu.cfs_quota_us.one_cpu │ │ └── cpu.cfs_quota_us.two_cpu │ ├── cpuacct_1 │ │ ├── cpuacct.stat │ │ └── cpuacct.usage │ ├── cpuacct_2 │ │ ├── cpuacct.stat │ │ └── cpuacct.usage │ ├── cpuacct_garbage │ │ ├── cpuacct.stat │ │ └── cpuacct.usage │ ├── cpuacct_incomplete │ │ ├── cpuacct.stat │ │ └── cpuacct.usage │ ├── memory │ │ ├── memory.limit_in_bytes │ │ ├── memory.memsw.limit_in_bytes │ │ ├── memory.memsw.usage_in_bytes │ │ ├── memory.stat │ │ └── memory.usage_in_bytes │ ├── memory_garbage │ │ ├── memory.limit_in_bytes │ │ ├── memory.memsw.limit_in_bytes │ │ ├── memory.memsw.usage_in_bytes │ │ ├── memory.stat │ │ └── memory.usage_in_bytes │ ├── memory_incomplete │ │ ├── memory.limit_in_bytes │ │ ├── memory.memsw.limit_in_bytes │ │ ├── memory.memsw.usage_in_bytes │ │ ├── memory.stat │ │ └── memory.usage_in_bytes │ ├── memory_missing_files │ │ └── memory.stat │ └── memory_without_swap │ │ ├── memory.limit_in_bytes │ │ ├── memory.stat │ │ └── memory.usage_in_bytes │ └── cgroup_v2 │ ├── cpu.max_2_cpus │ ├── cpu.max_default │ ├── cpu.max_garbage │ ├── cpu.max_half │ ├── cpu.stat_1 │ ├── cpu.stat_2 │ ├── cpu.stat_garbage │ ├── cpu.stat_incomplete │ ├── memory │ ├── memory.current │ ├── memory.max │ ├── memory.stat │ ├── memory.swap.current │ └── memory.swap.max │ ├── memory_garbage │ ├── memory.current │ ├── memory.max │ ├── memory.stat │ ├── memory.swap.current │ └── memory.swap.max │ ├── memory_incomplete │ ├── memory.current │ ├── memory.max │ ├── memory.stat │ ├── memory.swap.current │ └── memory.swap.max │ ├── memory_missing_files │ └── memory.stat │ └── memory_without_swap │ ├── memory.current │ ├── memory.max │ └── memory.stat ├── rust-toolchain └── src ├── cpu ├── cgroup.rs ├── cgroup_v1.rs ├── cgroup_v2.rs ├── mod.rs └── proc.rs ├── disk_stats.rs ├── disk_usage.rs ├── error.rs ├── lib.rs ├── load.rs ├── memory ├── cgroup.rs ├── cgroup_v1.rs ├── cgroup_v2.rs ├── mod.rs └── proc.rs ├── network.rs └── process_memory.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | .vagrant 4 | /tmp 5 | -------------------------------------------------------------------------------- /.semaphore/semaphore.yml: -------------------------------------------------------------------------------- 1 | version: v1.0 2 | name: probes-rs 3 | agent: 4 | machine: 5 | type: e1-standard-2 6 | os_image: ubuntu2004 7 | global_job_config: 8 | prologue: 9 | commands: 10 | - checkout 11 | blocks: 12 | - name: "CentOS 8" 13 | dependencies: [] 14 | task: 15 | jobs: 16 | - name: "Build + Test" 17 | commands: 18 | - docker build -t probes/centos_8 -f docker/centos_8/Dockerfile . 19 | - docker run --rm -v $(pwd):/probes -t probes/centos_8 /bin/bash -c "source /root/.cargo/env; cd /probes; cargo test" 20 | - name: "Fedora 31" 21 | dependencies: [] 22 | task: 23 | jobs: 24 | - name: "Build + Test" 25 | commands: 26 | - docker build -t probes/fedora_31 -f docker/fedora_31/Dockerfile . 27 | - docker run --rm -v $(pwd):/probes -t probes/fedora_31 /bin/bash -c "source /root/.cargo/env; cd /probes; cargo test" 28 | - name: "Ubuntu 14.04" 29 | dependencies: [] 30 | task: 31 | jobs: 32 | - name: "Build + Test" 33 | commands: 34 | - docker build -t probes/ubuntu_1404 -f docker/ubuntu_1404/Dockerfile . 35 | - docker run --rm -v $(pwd):/probes -t probes/ubuntu_1404 /bin/bash -c "source /root/.cargo/env; cd /probes; cargo test" 36 | - name: "Ubuntu 16.04" 37 | dependencies: [] 38 | task: 39 | jobs: 40 | - name: "Build + Test" 41 | commands: 42 | - docker build -t probes/ubuntu_1604 -f docker/ubuntu_1604/Dockerfile . 43 | - docker run --rm -v $(pwd):/probes -t probes/ubuntu_1604 /bin/bash -c "source /root/.cargo/env; cd /probes; cargo test" 44 | - name: "Ubuntu 18.04" 45 | dependencies: [] 46 | task: 47 | jobs: 48 | - name: "Build + Test" 49 | commands: 50 | - docker build -t probes/ubuntu_1804 -f docker/ubuntu_1804/Dockerfile . 51 | - docker run --rm -v $(pwd):/probes -t probes/ubuntu_1804 /bin/bash -c "source /root/.cargo/env; cd /probes; cargo test" 52 | - name: "Ubuntu 20.04" 53 | dependencies: [] 54 | task: 55 | jobs: 56 | - name: "Build + Test" 57 | commands: 58 | - docker build -t probes/ubuntu_2004 -f docker/ubuntu_2004/Dockerfile . 59 | - docker run --rm -v $(pwd):/probes -t probes/ubuntu_2004 /bin/bash -c "source /root/.cargo/env; cd /probes; cargo test" 60 | - name: "Ubuntu 22.04" 61 | dependencies: [] 62 | task: 63 | jobs: 64 | - name: "Build + Test" 65 | commands: 66 | - docker build -t probes/ubuntu_2204 -f docker/ubuntu_2204/Dockerfile . 67 | - docker run --rm -v $(pwd):/probes -t probes/ubuntu_2204 /bin/bash -c "source /root/.cargo/env; cd /probes; cargo test" 68 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.6.0 4 | 5 | - Normalize CPU metrics for cgroups v1 systems. When we can detect how many CPUs there are set up in the container's limits, we will normalize the CPU percentages to a maximum of 100%. This is a breaking change. 6 | - Support fractional CPUs for cgroups v2 metrics. Previously a CPU count of 0.5 would be interpreted as 1 CPU. Now it will be correctly seen as half a CPU. 7 | 8 | ## 0.5.4 9 | 10 | - Fix disk usage returning a Vec with no entries on Alpine Linux when the `df --local` command fails. 11 | 12 | ## 0.5.3 13 | 14 | - Support disk usage reporting (using `df`) on Alpine Linux. 15 | - When a disk mountpoint has no inodes usage percentage, skip the mountpoint, and report the inodes information successfully for the inodes that do have an inodes usage percentage. 16 | 17 | ## 0.5.2 18 | 19 | - Normalize CPU usage percentages for cgroups v2 systems 20 | 21 | ## 0.5.1 22 | 23 | - Add support for CPU total usage for `/proc` based systems (VMs). 24 | 25 | ## 0.5.0 26 | 27 | - Add support for cgroups v2 in CPU and memory metrics 28 | - Change memory metrics struct to allow for values being optional 29 | 30 | ## 0.4.3 31 | 32 | - Add shared memory metric function `Memory.shmem()`. 33 | 34 | ## 0.4.2 35 | 36 | - Support shared memory metric. For containers only cgroups v1 is supported. 37 | 38 | ## 0.2.0 39 | 40 | * Initial support for containers 41 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Please visit [docs.appsignal.com/appsignal/code-of-conduct.html](https://docs.appsignal.com/appsignal/code-of-conduct.html) for our Code of Conduct for this project. 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "probes" 3 | version = "0.7.0" 4 | authors = ["Thijs Cadier ", 5 | "Robert Beekman ", 6 | "Timon Vonk ", 7 | "Adam Yeats ", 8 | "Tom de Bruijn "] 9 | description = "Library to read out system stats from a machine running Unix" 10 | readme = "README.md" 11 | repository = "https://github.com/appsignal/probes-rs/" 12 | keywords = ["unix", "linux", "probe"] 13 | license = "MIT" 14 | edition = "2018" 15 | 16 | [dependencies] 17 | libc = "0.2" 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 the Probes developers. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | docker build -t probes/ubuntu_2204 -f docker/ubuntu_2204/Dockerfile . 3 | 4 | build-all: 5 | docker build -t probes/centos_8 -f docker/centos_8/Dockerfile . 6 | docker build -t probes/fedora_31 -f docker/fedora_31/Dockerfile . 7 | docker build -t probes/ubuntu_1404 -f docker/ubuntu_1404/Dockerfile . 8 | docker build -t probes/ubuntu_1604 -f docker/ubuntu_1604/Dockerfile . 9 | docker build -t probes/ubuntu_1804 -f docker/ubuntu_1804/Dockerfile . 10 | docker build -t probes/ubuntu_2004 -f docker/ubuntu_2004/Dockerfile . 11 | docker build -t probes/ubuntu_2204 -f docker/ubuntu_2204/Dockerfile . 12 | 13 | check: 14 | docker run --rm \ 15 | -v $(PWD)/tmp/.cargo/registry/cache/ubuntu_2204:/root/.cargo/registry/cache \ 16 | -v $(PWD)/tmp/.cargo/registry/index:/root/.cargo/registry/index \ 17 | -v $(PWD)/tmp/.cargo/registry/src:/root/.cargo/registry/src \ 18 | -v $(PWD):/probes -t probes/ubuntu_2204 \ 19 | /bin/bash -c "source /root/.cargo/env; cd /probes; cargo check" 20 | 21 | test: 22 | docker run --rm \ 23 | -v $(PWD)/tmp/.cargo/registry/cache/ubuntu_2204:/root/.cargo/registry/cache \ 24 | -v $(PWD)/tmp/.cargo/registry/index:/root/.cargo/registry/index \ 25 | -v $(PWD)/tmp/.cargo/registry/src:/root/.cargo/registry/src \ 26 | -v $(PWD):/probes -t probes/ubuntu_2204 \ 27 | /bin/bash -c "source /root/.cargo/env; cd /probes; cargo test" 28 | 29 | publish: 30 | docker run --rm \ 31 | -v $(PWD)/tmp/.cargo/registry/cache/ubuntu_2204:/root/.cargo/registry/cache \ 32 | -v $(PWD)/tmp/.cargo/registry/index:/root/.cargo/registry/index \ 33 | -v $(PWD)/tmp/.cargo/registry/src:/root/.cargo/registry/src \ 34 | -v $(PWD):/probes -it probes/ubuntu_2204 \ 35 | /bin/bash -c "source /root/.cargo/env; cd /probes; cargo login; cargo publish" 36 | 37 | test-all: 38 | docker run --rm \ 39 | -v $(PWD)/tmp/.cargo/registry/cache/centos_8:/root/.cargo/registry/cache \ 40 | -v $(PWD)/tmp/.cargo/registry/index:/root/.cargo/registry/index \ 41 | -v $(PWD)/tmp/.cargo/registry/src:/root/.cargo/registry/src \ 42 | -v $(PWD):/probes -t probes/centos_8 \ 43 | /bin/bash -c "source /root/.cargo/env; cd /probes; cargo test" 44 | docker run --rm \ 45 | -v $(PWD)/tmp/.cargo/registry/cache/fedora_31:/root/.cargo/registry/cache \ 46 | -v $(PWD)/tmp/.cargo/registry/index:/root/.cargo/registry/index \ 47 | -v $(PWD)/tmp/.cargo/registry/src:/root/.cargo/registry/src \ 48 | -v $(PWD):/probes -t probes/fedora_31 \ 49 | /bin/bash -c "source /root/.cargo/env; cd /probes; cargo test" 50 | docker run --rm \ 51 | -v $(PWD)/tmp/.cargo/registry/cache/ubuntu_1404:/root/.cargo/registry/cache \ 52 | -v $(PWD)/tmp/.cargo/registry/index:/root/.cargo/registry/index \ 53 | -v $(PWD)/tmp/.cargo/registry/src:/root/.cargo/registry/src \ 54 | -v $(PWD):/probes -t probes/ubuntu_1404 \ 55 | /bin/bash -c "source /root/.cargo/env; cd /probes; cargo test" 56 | docker run --rm \ 57 | -v $(PWD)/tmp/.cargo/registry/cache/ubuntu_1604:/root/.cargo/registry/cache \ 58 | -v $(PWD)/tmp/.cargo/registry/index:/root/.cargo/registry/index \ 59 | -v $(PWD)/tmp/.cargo/registry/src:/root/.cargo/registry/src \ 60 | -v $(PWD):/probes -t probes/ubuntu_1604 \ 61 | /bin/bash -c "source /root/.cargo/env; cd /probes; cargo test" 62 | docker run --rm \ 63 | -v $(PWD)/tmp/.cargo/registry/cache/ubuntu_1804:/root/.cargo/registry/cache \ 64 | -v $(PWD)/tmp/.cargo/registry/index:/root/.cargo/registry/index \ 65 | -v $(PWD)/tmp/.cargo/registry/src:/root/.cargo/registry/src \ 66 | -v $(PWD):/probes -t probes/ubuntu_1804 \ 67 | /bin/bash -c "source /root/.cargo/env; cd /probes; cargo test" 68 | docker run --rm \ 69 | -v $(PWD)/tmp/.cargo/registry/cache/ubuntu_2004:/root/.cargo/registry/cache \ 70 | -v $(PWD)/tmp/.cargo/registry/index:/root/.cargo/registry/index \ 71 | -v $(PWD)/tmp/.cargo/registry/src:/root/.cargo/registry/src \ 72 | -v $(PWD):/probes -t probes/ubuntu_2004 \ 73 | /bin/bash -c "source /root/.cargo/env; cd /probes; cargo test" 74 | docker run --rm \ 75 | -v $(PWD)/tmp/.cargo/registry/cache/ubuntu_2204:/root/.cargo/registry/cache \ 76 | -v $(PWD)/tmp/.cargo/registry/index:/root/.cargo/registry/index \ 77 | -v $(PWD)/tmp/.cargo/registry/src:/root/.cargo/registry/src \ 78 | -v $(PWD):/probes -t probes/ubuntu_2204 \ 79 | /bin/bash -c "source /root/.cargo/env; cd /probes; cargo test" 80 | 81 | test-cgroups-v1: 82 | vagrant up cgroups_v1 83 | vagrant ssh cgroups_v1 -c "cd /vagrant; sudo -E make build test" 84 | 85 | test-cgroups-v2: 86 | vagrant up cgroups_v2 87 | vagrant ssh cgroups_v2 -c "cd /vagrant; sudo -E make build test" 88 | 89 | test-cgroups: test-cgroups-v1 test-cgroups-v2 90 | @echo '' 91 | @echo 'Done! If desired, run `vagrant halt` to stop the machines, or `vagrant destroy -f` to destroy them.' 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Probes 2 | 3 | [![Build Status](https://travis-ci.org/appsignal/probes-rs.svg?branch=main)](https://travis-ci.org/appsignal/probes-rs) 4 | [![Crate](http://meritbadge.herokuapp.com/probes)](https://crates.io/crates/probes) 5 | 6 | Rust library to read out system stats from a machine running Unix. 7 | Currently only supports Linux. 8 | 9 | ## Supported stats 10 | 11 | ### System wide 12 | 13 | * load 14 | * cpu 15 | * memory 16 | * network 17 | * io 18 | * disk 19 | 20 | ### Per process 21 | 22 | * memory (total, resident, virtual) 23 | 24 | ## Contributing 25 | 26 | Thinking of contributing to our Probes package? Awesome! 🚀 27 | 28 | Please follow our [Contributing guide][contributing-guide] in our 29 | documentation and follow our [Code of Conduct][coc]. 30 | 31 | Running `cargo fmt` before contributing changes would reduce diffs for future 32 | contributions. 33 | 34 | Also, we would be very happy to send you Stroopwafles. Have look at everyone 35 | we send a package to so far on our [Stroopwafles page][waffles-page]. 36 | 37 | ## Setup 38 | 39 | * Download and install [Docker](https://www.docker.com/) 40 | * Build the images: `make build` 41 | * Make sure that the path where this code resided can be mounted as 42 | a volume with Docker. 43 | * Run the tests on all images: `make test` 44 | * Add awesome features! 45 | 46 | The tests on Travis are only run directly on that VM. Make sure to run 47 | the full test suite manually before every release. 48 | 49 | ## Release 50 | 51 | - Update version in `Cargo.toml`. 52 | - Update `CHANGELOG.md` file. 53 | - Commit your changes. 54 | - Tag the release: `git tag v#.#.#`. 55 | - Run `cargo publish`. 56 | 57 | [contributing-guide]: http://docs.appsignal.com/appsignal/contributing.html 58 | [coc]: https://docs.appsignal.com/appsignal/code-of-conduct.html 59 | [waffles-page]: https://appsignal.com/waffles 60 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | Vagrant.configure("2") do |config| 2 | config.vm.define "cgroups_v1" do |v1| 3 | v1.vm.box = "bento/ubuntu-20.04" 4 | end 5 | 6 | config.vm.define "cgroups_v2" do |v1| 7 | v1.vm.box = "bento/ubuntu-22.04" 8 | end 9 | 10 | config.vm.provision "shell", inline: <<-EOF.chomp 11 | sudo apt-get update 12 | 13 | sudo apt-get -y install \ 14 | ca-certificates \ 15 | curl \ 16 | gnupg \ 17 | lsb-release \ 18 | make 19 | 20 | sudo mkdir -m 0755 -p /etc/apt/keyrings 21 | 22 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg 23 | 24 | echo \ 25 | "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ 26 | $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null 27 | 28 | sudo apt-get update 29 | 30 | sudo chmod a+r /etc/apt/keyrings/docker.gpg 31 | 32 | sudo apt-get update 33 | 34 | sudo apt-get -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin 35 | 36 | # Test it went well 37 | sudo docker run hello-world 38 | sudo docker info 39 | EOF 40 | end 41 | -------------------------------------------------------------------------------- /docker/centos_8/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM centos:centos8 2 | RUN cd /etc/yum.repos.d/ && \ 3 | sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-* && \ 4 | sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-* 5 | RUN yum -y update && yum clean all 6 | RUN yum -y groupinstall 'Development Tools' 7 | 8 | RUN curl https://sh.rustup.rs > sh.rustup.rs 9 | RUN sh sh.rustup.rs --default-toolchain none -y \ 10 | && echo 'source $HOME/.cargo/env' >> $HOME/.bashrc 11 | 12 | WORKDIR /probes 13 | 14 | COPY rust-toolchain . 15 | RUN . $HOME/.cargo/env && rustup toolchain install "$(cat rust-toolchain)" 16 | RUN rm rust-toolchain 17 | -------------------------------------------------------------------------------- /docker/fedora_31/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM fedora:31 2 | RUN yum -y update; yum clean all 3 | RUN yum -y groupinstall 'Development Tools' 4 | 5 | RUN curl https://sh.rustup.rs > sh.rustup.rs 6 | RUN sh sh.rustup.rs --default-toolchain none -y \ 7 | && echo 'source $HOME/.cargo/env' >> $HOME/.bashrc 8 | 9 | WORKDIR /probes 10 | 11 | COPY rust-toolchain . 12 | RUN . $HOME/.cargo/env && rustup toolchain install "$(cat rust-toolchain)" 13 | RUN rm rust-toolchain 14 | -------------------------------------------------------------------------------- /docker/ubuntu_1404/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:14.04 2 | RUN apt-get update 3 | RUN apt-get upgrade -y 4 | RUN apt-get install -y curl file sudo build-essential 5 | 6 | RUN curl https://sh.rustup.rs > sh.rustup.rs 7 | RUN sh sh.rustup.rs --default-toolchain none -y \ 8 | && echo 'source $HOME/.cargo/env' >> $HOME/.bashrc 9 | 10 | WORKDIR /probes 11 | 12 | COPY rust-toolchain . 13 | RUN . $HOME/.cargo/env && rustup toolchain install "$(cat rust-toolchain)" 14 | RUN rm rust-toolchain 15 | -------------------------------------------------------------------------------- /docker/ubuntu_1604/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 2 | RUN apt-get update 3 | RUN apt-get upgrade -y 4 | RUN apt-get install -y curl file sudo build-essential 5 | 6 | RUN curl https://sh.rustup.rs > sh.rustup.rs 7 | RUN sh sh.rustup.rs --default-toolchain none -y \ 8 | && echo 'source $HOME/.cargo/env' >> $HOME/.bashrc 9 | 10 | WORKDIR /probes 11 | 12 | COPY rust-toolchain . 13 | RUN . $HOME/.cargo/env && rustup toolchain install "$(cat rust-toolchain)" 14 | RUN rm rust-toolchain 15 | -------------------------------------------------------------------------------- /docker/ubuntu_1804/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | RUN apt-get update 3 | RUN apt-get upgrade -y 4 | RUN apt-get install -y curl file sudo build-essential 5 | 6 | RUN curl https://sh.rustup.rs > sh.rustup.rs 7 | RUN sh sh.rustup.rs --default-toolchain none -y \ 8 | && echo 'source $HOME/.cargo/env' >> $HOME/.bashrc 9 | 10 | WORKDIR /probes 11 | 12 | COPY rust-toolchain . 13 | RUN . $HOME/.cargo/env && rustup toolchain install "$(cat rust-toolchain)" 14 | RUN rm rust-toolchain 15 | -------------------------------------------------------------------------------- /docker/ubuntu_2004/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | RUN apt-get update 3 | RUN apt-get upgrade -y 4 | RUN apt-get install -y curl file sudo build-essential 5 | 6 | RUN curl https://sh.rustup.rs > sh.rustup.rs 7 | RUN sh sh.rustup.rs --default-toolchain none -y \ 8 | && echo 'source $HOME/.cargo/env' >> $HOME/.bashrc 9 | 10 | WORKDIR /probes 11 | 12 | COPY rust-toolchain . 13 | RUN . $HOME/.cargo/env && rustup toolchain install "$(cat rust-toolchain)" 14 | RUN rm rust-toolchain 15 | -------------------------------------------------------------------------------- /docker/ubuntu_2204/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | RUN apt-get update 3 | RUN apt-get upgrade -y 4 | RUN apt-get install -y curl file sudo build-essential 5 | 6 | RUN curl https://sh.rustup.rs > sh.rustup.rs 7 | RUN sh sh.rustup.rs --default-toolchain none -y \ 8 | && echo 'source $HOME/.cargo/env' >> $HOME/.bashrc 9 | 10 | WORKDIR /probes 11 | 12 | COPY rust-toolchain . 13 | RUN . $HOME/.cargo/env && rustup toolchain install "$(cat rust-toolchain)" 14 | RUN rm rust-toolchain 15 | -------------------------------------------------------------------------------- /fixtures/linux/cpu/proc_stat: -------------------------------------------------------------------------------- 1 | cpu 10 3 7 6 5 4 3 1 2 1 2 | cpu0 561319 5566 222790 10391434 39539 0 28413 0 0 0 3 | cpu1 506047 5687 196253 64662 162 0 1306 0 0 0 4 | cpu2 524066 5683 313970 64187 33 0 1740 0 0 0 5 | cpu3 541179 5500 265217 65085 68 0 462 0 0 0 6 | intr 70555899 20 0 0 0 0 0 0 0 1 1137907 0 0 0 0 0 0 3193584 0 8418743 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 926754 1769112 15 6181 0 12 135 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 7 | ctxt 1555131703 8 | btime 1444814406 9 | processes 661401 10 | procs_running 1 11 | procs_blocked 0 12 | softirq 80795295 166 26243529 2166 3574938 3091764 54 10113048 19346678 0 18422952 13 | -------------------------------------------------------------------------------- /fixtures/linux/cpu/proc_stat_1: -------------------------------------------------------------------------------- 1 | cpu 26897 200 11452 1385537 598 3425 95 200 100 50 2 | cpu0 11191 0 1933 698943 268 101 74 0 0 3 | cpu1 15706 0 9519 686594 330 3324 21 0 0 4 | intr 1687340 36 7 0 0 0 0 0 0 0 0 0 0 106 0 62 62 0 0 0 13054 1283214 6990 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 5 | ctxt 2830293 6 | btime 1462093486 7 | processes 33160 8 | procs_running 1 9 | procs_blocked 0 10 | softirq 432696 0 121592 1422 12916 6655 0 4 36579 120 253408 11 | -------------------------------------------------------------------------------- /fixtures/linux/cpu/proc_stat_2: -------------------------------------------------------------------------------- 1 | cpu 28076 300 11829 1408460 610 3549 102 300 200 100 2 | cpu0 11770 0 1980 711022 280 102 81 0 0 3 | cpu1 16306 0 9849 698338 330 3447 21 0 0 4 | intr 1751003 36 7 0 0 0 0 0 0 0 0 0 0 106 0 62 62 0 0 0 13323 1335454 7311 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 5 | ctxt 2941249 6 | btime 1462093485 7 | processes 34214 8 | procs_running 1 9 | procs_blocked 0 10 | softirq 445635 0 125344 1448 13184 6967 0 4 37642 121 260925 11 | -------------------------------------------------------------------------------- /fixtures/linux/cpu/proc_stat_garbage: -------------------------------------------------------------------------------- 1 | Ph'nglui mglw'nafh Cthulhu R'lyeh wgah'nagl fhtagn 2 | -------------------------------------------------------------------------------- /fixtures/linux/cpu/proc_stat_incomplete: -------------------------------------------------------------------------------- 1 | cpu 0 1 2 | -------------------------------------------------------------------------------- /fixtures/linux/cpu/proc_stat_partial: -------------------------------------------------------------------------------- 1 | cpu 10 3 7 6 5 2 | cpu0 561319 5566 222790 10391434 39539 0 28413 0 0 0 3 | cpu1 506047 5687 196253 64662 162 0 1306 0 0 0 4 | cpu2 524066 5683 313970 64187 33 0 1740 0 0 0 5 | cpu3 541179 5500 265217 65085 68 0 462 0 0 0 6 | intr 70555899 20 0 0 0 0 0 0 0 1 1137907 0 0 0 0 0 0 3193584 0 8418743 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 926754 1769112 15 6181 0 12 135 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 7 | ctxt 1555131703 8 | btime 1444814406 9 | processes 661401 10 | procs_running 1 11 | procs_blocked 0 12 | softirq 80795295 166 26243529 2166 3574938 3091764 54 10113048 19346678 0 18422952 13 | -------------------------------------------------------------------------------- /fixtures/linux/disk_stats/proc_diskstats: -------------------------------------------------------------------------------- 1 | 8 0 sda 6185 9367 403272 22160 2591 8251 84452 2860 0 8960 24990 2 | 8 1 sda1 483 4782 41466 1100 7 1 28 40 0 930 1140 3 | -------------------------------------------------------------------------------- /fixtures/linux/disk_stats/proc_diskstats_4_18: -------------------------------------------------------------------------------- 1 | 8 0 sda 6185 9367 403272 22160 2591 8251 84452 2860 0 8960 24990 0 0 0 0 2 | 8 1 sda1 483 4782 41466 1100 7 1 28 40 0 930 1140 0 0 0 0 3 | -------------------------------------------------------------------------------- /fixtures/linux/disk_stats/proc_diskstats_5_5: -------------------------------------------------------------------------------- 1 | 202 0 xvda 11514 271 1204763 14486 130975 18219 2798097 178789 0 42381 112571 0 0 0 0 0 0 2 | 202 1 xvda1 169 34 8894 115 10 6 44 7 0 99 51 0 0 0 0 0 0 3 | 202 2 xvda2 258 216 11969 238 1 0 1 0 0 98 135 0 0 0 0 0 0 4 | 202 3 xvda3 26 0 208 13 0 0 0 0 0 13 5 0 0 0 0 0 0 5 | 202 4 xvda4 11004 21 1181180 14094 130964 18213 2798052 178781 0 42317 112377 0 0 0 0 0 0 6 | 202 80 xvdf 25663 55 810823 29159 33077 21044 1430229 37739 0 42594 33450 0 0 0 0 0 0 7 | 202 81 xvdf1 25621 55 808479 29142 33077 21044 1430229 37739 0 42583 33447 0 0 0 0 0 0 8 | -------------------------------------------------------------------------------- /fixtures/linux/disk_stats/proc_diskstats_garbage: -------------------------------------------------------------------------------- 1 | Ph'nglui mglw'nafh Cthulhu R'lyeh wgah'nagl fhtagn 2 | -------------------------------------------------------------------------------- /fixtures/linux/disk_stats/proc_diskstats_incomplete: -------------------------------------------------------------------------------- 1 | 1 0 ram0 0 0 0 0 0 0 0 0 0 0 0 2 | 1 1 ram1 0 0 0 0 0 0 0 0 0 0 0 3 | 1 2 ram2 0 0 0 0 0 0 0 0 0 0 0 4 | 1 3 ram3 0 0 0 0 0 0 0 0 0 0 0 5 | 1 4 ram4 0 0 0 0 0 0 0 0 0 0 0 6 | 1 5 ram5 0 0 0 0 0 0 0 0 0 0 0 7 | 1 6 ram6 0 0 0 0 0 0 0 0 0 0 0 8 | 1 7 ram7 0 0 0 0 0 0 0 0 0 0 0 9 | 1 8 ram8 0 0 0 0 0 0 0 0 0 0 0 10 | 1 9 ram9 0 0 0 0 0 0 0 0 0 0 0 11 | 1 10 ram10 0 0 0 0 0 0 0 0 0 0 0 12 | 1 11 ram11 0 0 0 0 0 0 0 0 0 0 0 13 | 1 12 ram12 0 0 0 0 0 0 0 0 0 0 0 14 | 1 13 ram13 0 0 15 | 1 14 ram14 0 0 16 | 1 15 ram15 0 0 17 | 7 0 loop0 0 0 18 | 7 1 loop1 0 0 19 | 7 2 loop2 0 0 20 | 7 3 loop3 0 0 21 | 7 4 loop4 0 0 22 | 7 5 loop5 0 0 23 | 7 6 loop6 0 0 24 | 7 7 loop7 0 0 25 | 11 0 sr0 0 0 0 26 | 11 1 sr1 0 0 0 27 | 8 0 sda 6185 28 | 8 1 sda1 483 29 | 8 2 sda2 2 0 30 | 8 5 sda5 5651 31 | 251 0 dm-0 1006 32 | 251 1 dm-1 113 33 | -------------------------------------------------------------------------------- /fixtures/linux/disk_usage/df: -------------------------------------------------------------------------------- 1 | Filesystem 1K-blocks Used Available Use% Mounted on 2 | /dev/mapper/lucid64-root 3 | 81234688 2344444 74763732 4% / 4 | none 183176 180 182996 1% /dev 5 | /dev/sda1 233191 17217 203533 8% /boot 6 | -------------------------------------------------------------------------------- /fixtures/linux/disk_usage/df_garbage: -------------------------------------------------------------------------------- 1 | Filesystem 1K-blocks Used Available Use% Mounted on 2 | Ph'nglui mglw'nafhuCthulhu R'lyehjwgah'nagl fhtagn 3 | -------------------------------------------------------------------------------- /fixtures/linux/disk_usage/df_i: -------------------------------------------------------------------------------- 1 | Filesystem Inodes IUsed IFree IUse% Mounted on 2 | overlay 2097152 122591 1974561 6% / 3 | tmpfs 254863 16 254847 1% /dev 4 | tmpfs 254863 15 254848 1% /sys/fs/cgroup 5 | -------------------------------------------------------------------------------- /fixtures/linux/disk_usage/df_i_dash_percentage: -------------------------------------------------------------------------------- 1 | Filesystem Inodes IUsed IFree IUse% Mounted on 2 | overlay 2097152 122591 1974561 6% / 3 | tmpfs 254863 16 254847 - /dev 4 | -------------------------------------------------------------------------------- /fixtures/linux/disk_usage/df_incomplete: -------------------------------------------------------------------------------- 1 | Filesystem 1K-blocks 2 | /dev/mapper/lucid64-root 3 | 81234688 4 | none 183176 5 | none 188032 6 | none 188032 7 | none 188032 8 | none 188032 9 | /dev/sda1 233191 10 | vagrant 243950084 11 | -------------------------------------------------------------------------------- /fixtures/linux/load/proc_loadavg: -------------------------------------------------------------------------------- 1 | 0.01 0.02 0.03 1/92 24892 2 | -------------------------------------------------------------------------------- /fixtures/linux/load/proc_loadavg_garbage: -------------------------------------------------------------------------------- 1 | aaa 0.02 0.03 1/92 24892 2 | -------------------------------------------------------------------------------- /fixtures/linux/load/proc_loadavg_incomplete: -------------------------------------------------------------------------------- 1 | 0.01 0.02 2 | -------------------------------------------------------------------------------- /fixtures/linux/memory/proc_meminfo: -------------------------------------------------------------------------------- 1 | MemTotal: 376072 kB 2 | MemFree: 125104 kB 3 | Buffers: 22820 kB 4 | Cached: 176324 kB 5 | SwapCached: 336 kB 6 | Active: 113260 kB 7 | Inactive: 93196 kB 8 | Active(anon): 360 kB 9 | Inactive(anon): 7484 kB 10 | Active(file): 112900 kB 11 | Inactive(file): 85712 kB 12 | Unevictable: 0 kB 13 | Mlocked: 0 kB 14 | SwapTotal: 1101816 kB 15 | SwapFree: 1100644 kB 16 | Dirty: 0 kB 17 | Writeback: 0 kB 18 | AnonPages: 6996 kB 19 | Mapped: 5128 kB 20 | Shmem: 548 kB 21 | Slab: 27196 kB 22 | SReclaimable: 19032 kB 23 | SUnreclaim: 8164 kB 24 | KernelStack: 728 kB 25 | PageTables: 1300 kB 26 | NFS_Unstable: 0 kB 27 | Bounce: 0 kB 28 | WritebackTmp: 0 kB 29 | CommitLimit: 1289852 kB 30 | Committed_AS: 51788 kB 31 | VmallocTotal: 34359738367 kB 32 | VmallocUsed: 20712 kB 33 | VmallocChunk: 34359712244 kB 34 | HardwareCorrupted: 0 kB 35 | HugePages_Total: 0 36 | HugePages_Free: 0 37 | HugePages_Rsvd: 0 38 | HugePages_Surp: 0 39 | Hugepagesize: 2048 kB 40 | DirectMap4k: 8128 kB 41 | DirectMap2M: 385024 kB 42 | -------------------------------------------------------------------------------- /fixtures/linux/memory/proc_meminfo_garbage: -------------------------------------------------------------------------------- 1 | MemTotal: aaaa kB 2 | MemFree: 125104 kB 3 | Buffers: 22820 kB 4 | Cached: 176324 kB 5 | SwapCached: 336 kB 6 | Active: 113260 kB 7 | Inactive: 93196 kB 8 | Active(anon): 360 kB 9 | Inactive(anon): 7484 kB 10 | Active(file): 112900 kB 11 | Inactive(file): 85712 kB 12 | Unevictable: 0 kB 13 | Mlocked: 0 kB 14 | SwapTotal: 1101816 kB 15 | SwapFree: 1100644 kB 16 | Dirty: 0 kB 17 | Writeback: 0 kB 18 | AnonPages: 6996 kB 19 | Mapped: 5128 kB 20 | Shmem: 548 kB 21 | Slab: 27196 kB 22 | SReclaimable: 19032 kB 23 | SUnreclaim: 8164 kB 24 | KernelStack: 728 kB 25 | PageTables: 1300 kB 26 | NFS_Unstable: 0 kB 27 | Bounce: 0 kB 28 | WritebackTmp: 0 kB 29 | CommitLimit: 1289852 kB 30 | Committed_AS: 51788 kB 31 | VmallocTotal: 34359738367 kB 32 | VmallocUsed: 20712 kB 33 | VmallocChunk: 34359712244 kB 34 | HardwareCorrupted: 0 kB 35 | HugePages_Total: 0 36 | HugePages_Free: 0 37 | HugePages_Rsvd: 0 38 | HugePages_Surp: 0 39 | Hugepagesize: 2048 kB 40 | DirectMap4k: 8128 kB 41 | DirectMap2M: 385024 kB 42 | 43 | -------------------------------------------------------------------------------- /fixtures/linux/memory/proc_meminfo_incomplete: -------------------------------------------------------------------------------- 1 | MemTotal: 376072 kB 2 | MemFree: 125104 kB 3 | -------------------------------------------------------------------------------- /fixtures/linux/network/proc_net_dev: -------------------------------------------------------------------------------- 1 | Inter-| Receive | Transmit 2 | face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed 3 | lo: 560 8 0 0 0 0 0 0 560 8 0 0 0 0 0 0 4 | eth0: 254972 1129 0 0 0 0 0 0 72219 711 0 0 0 0 0 0 5 | eth1: 354972 1129 0 0 0 0 0 0 82219 711 0 0 0 0 0 0 6 | -------------------------------------------------------------------------------- /fixtures/linux/network/proc_net_dev_garbage: -------------------------------------------------------------------------------- 1 | Inter-| Receive | Transmit 2 | face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed 3 | lo: 560 8 0 0 0 0 0 0 560 8 0 0 0 0 0 0 4 | eth0: aaaaaa 1129 0 0 0 0 0 0 72219 711 0 0 0 0 0 0 5 | eth1: 354972 1129 0 0 0 0 0 0 82219 711 0 0 0 0 0 0 6 | -------------------------------------------------------------------------------- /fixtures/linux/network/proc_net_dev_incomplete: -------------------------------------------------------------------------------- 1 | Inter-| Receive | Transmit 2 | face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed 3 | lo: 560 8 0 0 0 0 0 0 560 8 0 0 0 0 0 0 4 | eth0: 254972 1129 0 5 | -------------------------------------------------------------------------------- /fixtures/linux/proc/self/cgroup/docker: -------------------------------------------------------------------------------- 1 | 14:name=systemd:/docker/0c703b75cdeaad7c933aa68b4678cc5c37a12d5ef5d7cb52c9cefe684d98e575 2 | 13:pids:/docker/0c703b75cdeaad7c933aa68b4678cc5c37a12d5ef5d7cb52c9cefe684d98e575 3 | 12:hugetlb:/docker/0c703b75cdeaad7c933aa68b4678cc5c37a12d5ef5d7cb52c9cefe684d98e575 4 | 11:net_prio:/docker/0c703b75cdeaad7c933aa68b4678cc5c37a12d5ef5d7cb52c9cefe684d98e575 5 | 10:perf_event:/docker/0c703b75cdeaad7c933aa68b4678cc5c37a12d5ef5d7cb52c9cefe684d98e575 6 | 9:net_cls:/docker/0c703b75cdeaad7c933aa68b4678cc5c37a12d5ef5d7cb52c9cefe684d98e575 7 | 8:freezer:/docker/0c703b75cdeaad7c933aa68b4678cc5c37a12d5ef5d7cb52c9cefe684d98e575 8 | 7:devices:/docker/0c703b75cdeaad7c933aa68b4678cc5c37a12d5ef5d7cb52c9cefe684d98e575 9 | 6:memory:/docker/0c703b75cdeaad7c933aa68b4678cc5c37a12d5ef5d7cb52c9cefe684d98e575 10 | 5:blkio:/docker/0c703b75cdeaad7c933aa68b4678cc5c37a12d5ef5d7cb52c9cefe684d98e575 11 | 4:cpuacct:/docker/0c703b75cdeaad7c933aa68b4678cc5c37a12d5ef5d7cb52c9cefe684d98e575 12 | 3:cpu:/docker/0c703b75cdeaad7c933aa68b4678cc5c37a12d5ef5d7cb52c9cefe684d98e575 13 | 2:cpuset:/docker/0c703b75cdeaad7c933aa68b4678cc5c37a12d5ef5d7cb52c9cefe684d98e575 14 | 1:name=openrc:/docker 15 | -------------------------------------------------------------------------------- /fixtures/linux/proc/self/cgroup/docker_systemd: -------------------------------------------------------------------------------- 1 | 8:net_cls:/system.slice/docker-09f1c4d420025670a3633edbc9b31450f1d6b2ff87b5912a10c320ad398c7215.scope 2 | 7:freezer:/system.slice/docker-09f1c4d420025670a3633edbc9b31450f1d6b2ff87b5912a10c320ad398c7215.scope 3 | 6:devices:/system.slice/docker-09f1c4d420025670a3633edbc9b31450f1d6b2ff87b5912a10c320ad398c7215.scope 4 | 5:memory:/system.slice/docker-09f1c4d420025670a3633edbc9b31450f1d6b2ff87b5912a10c320ad398c7215.scope 5 | 4:blkio:/system.slice/docker-09f1c4d420025670a3633edbc9b31450f1d6b2ff87b5912a10c320ad398c7215.scope 6 | 3:cpu,cpuacct:/system.slice/docker-09f1c4d420025670a3633edbc9b31450f1d6b2ff87b5912a10c320ad398c7215.scope 7 | 2:cpuset:/system.slice/docker-09f1c4d420025670a3633edbc9b31450f1d6b2ff87b5912a10c320ad398c7215.scope 8 | 1:name=openrc:/system.slice/docker-09f1c4d420025670a3633edbc9b31450f1d6b2ff87b5912a10c320ad398c7215.scope 9 | -------------------------------------------------------------------------------- /fixtures/linux/proc/self/cgroup/kubernetes: -------------------------------------------------------------------------------- 1 | 11:hugetlb:/kubepods/besteffort/pod75f47a71-6279-11e8-aeff-08002750f0f7/f69dc40a90102b2897bb0d4a010fa9adf88f20f58b9707f643cc84e510c28e57 2 | 10:net_cls:/kubepods/besteffort/pod75f47a71-6279-11e8-aeff-08002750f0f7/f69dc40a90102b2897bb0d4a010fa9adf88f20f58b9707f643cc84e510c28e57 3 | 9:blkio:/kubepods/besteffort/pod75f47a71-6279-11e8-aeff-08002750f0f7/f69dc40a90102b2897bb0d4a010fa9adf88f20f58b9707f643cc84e510c28e57 4 | 8:memory:/kubepods/besteffort/pod75f47a71-6279-11e8-aeff-08002750f0f7/f69dc40a90102b2897bb0d4a010fa9adf88f20f58b9707f643cc84e510c28e57 5 | 7:devices:/kubepods/besteffort/pod75f47a71-6279-11e8-aeff-08002750f0f7/f69dc40a90102b2897bb0d4a010fa9adf88f20f58b9707f643cc84e510c28e57 6 | 6:perf_event:/kubepods/besteffort/pod75f47a71-6279-11e8-aeff-08002750f0f7/f69dc40a90102b2897bb0d4a010fa9adf88f20f58b9707f643cc84e510c28e57 7 | 5:cpu,cpuacct:/kubepods/besteffort/pod75f47a71-6279-11e8-aeff-08002750f0f7/f69dc40a90102b2897bb0d4a010fa9adf88f20f58b9707f643cc84e510c28e57 8 | 4:pids:/kubepods/besteffort/pod75f47a71-6279-11e8-aeff-08002750f0f7/f69dc40a90102b2897bb0d4a010fa9adf88f20f58b9707f643cc84e510c28e57 9 | 3:freezer:/kubepods/besteffort/pod75f47a71-6279-11e8-aeff-08002750f0f7/f69dc40a90102b2897bb0d4a010fa9adf88f20f58b9707f643cc84e510c28e57 10 | 2:cpuset:/kubepods/besteffort/pod75f47a71-6279-11e8-aeff-08002750f0f7/f69dc40a90102b2897bb0d4a010fa9adf88f20f58b9707f643cc84e510c28e57 11 | 1:name=systemd:/kubepods/besteffort/pod75f47a71-6279-11e8-aeff-08002750f0f7/f69dc40a90102b2897bb0d4a010fa9adf88f20f58b9707f643cc84e510c28e57 12 | -------------------------------------------------------------------------------- /fixtures/linux/proc/self/cgroup/lxc: -------------------------------------------------------------------------------- 1 | 11:name=systemd:/lxc/1a2e485e-3947-4bb6-8c24-8774f0859648 2 | 10:hugetlb:/lxc/1a2e485e-3947-4bb6-8c24-8774f0859648 3 | 9:perf_event:/lxc/1a2e485e-3947-4bb6-8c24-8774f0859648 4 | 8:blkio:/lxc/1a2e485e-3947-4bb6-8c24-8774f0859648 5 | 7:freezer:/lxc/1a2e485e-3947-4bb6-8c24-8774f0859648 6 | 6:devices:/lxc/1a2e485e-3947-4bb6-8c24-8774f0859648 7 | 5:memory:/lxc/1a2e485e-3947-4bb6-8c24-8774f0859648 8 | 4:cpuacct:/lxc/1a2e485e-3947-4bb6-8c24-8774f0859648 9 | 3:cpu:/lxc/1a2e485e-3947-4bb6-8c24-8774f0859648 10 | 2:cpuset:/lxc/1a2e485e-3947-4bb6-8c24-8774f0859648 11 | -------------------------------------------------------------------------------- /fixtures/linux/proc/self/cgroup/none: -------------------------------------------------------------------------------- 1 | 2:name=systemd:/user/0.user/1.session 2 | -------------------------------------------------------------------------------- /fixtures/linux/process_memory/proc_self_statm: -------------------------------------------------------------------------------- 1 | 6373 1138 428 214 0 755 0 2 | -------------------------------------------------------------------------------- /fixtures/linux/process_memory/proc_self_statm_garbage: -------------------------------------------------------------------------------- 1 | eceveveev 2 | -------------------------------------------------------------------------------- /fixtures/linux/process_memory/proc_self_statm_incomplete: -------------------------------------------------------------------------------- 1 | 1086 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v1/cpu_quota/cpu.cfs_period_us: -------------------------------------------------------------------------------- 1 | 100000 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v1/cpu_quota/cpu.cfs_quota_us.half_cpu: -------------------------------------------------------------------------------- 1 | 50000 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v1/cpu_quota/cpu.cfs_quota_us.minus_one: -------------------------------------------------------------------------------- 1 | -1 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v1/cpu_quota/cpu.cfs_quota_us.one_cpu: -------------------------------------------------------------------------------- 1 | 100000 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v1/cpu_quota/cpu.cfs_quota_us.two_cpu: -------------------------------------------------------------------------------- 1 | 200000 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v1/cpuacct_1/cpuacct.stat: -------------------------------------------------------------------------------- 1 | user 14934 2 | system 98 3 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v1/cpuacct_1/cpuacct.usage: -------------------------------------------------------------------------------- 1 | 152657213021 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v1/cpuacct_2/cpuacct.stat: -------------------------------------------------------------------------------- 1 | user 17783 2 | system 121 3 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v1/cpuacct_2/cpuacct.usage: -------------------------------------------------------------------------------- 1 | 182405617026 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v1/cpuacct_garbage/cpuacct.stat: -------------------------------------------------------------------------------- 1 | user aaaa 2 | system bbbb 3 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v1/cpuacct_garbage/cpuacct.usage: -------------------------------------------------------------------------------- 1 | cccccccccccc 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v1/cpuacct_incomplete/cpuacct.stat: -------------------------------------------------------------------------------- 1 | user 404 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v1/cpuacct_incomplete/cpuacct.usage: -------------------------------------------------------------------------------- 1 | 13953750329 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v1/memory/memory.limit_in_bytes: -------------------------------------------------------------------------------- 1 | 524288000 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v1/memory/memory.memsw.limit_in_bytes: -------------------------------------------------------------------------------- 1 | 2048000000 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v1/memory/memory.memsw.usage_in_bytes: -------------------------------------------------------------------------------- 1 | 512000000 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v1/memory/memory.stat: -------------------------------------------------------------------------------- 1 | cache 60342272 2 | rss 1445888 3 | rss_huge 0 4 | mapped_file 3710976 5 | dirty 0 6 | writeback 0 7 | swap 0 8 | pgpgin 56963 9 | pgpgout 53120 10 | pgfault 87136 11 | pgmajfault 149 12 | inactive_anon 8192 13 | active_anon 1482752 14 | inactive_file 19841024 15 | active_file 40439808 16 | unevictable 0 17 | hierarchical_memory_limit 524288000 18 | hierarchical_memsw_limit 1073741824 19 | total_cache 60342272 20 | total_rss 1445888 21 | total_rss_huge 0 22 | total_mapped_file 3710976 23 | total_dirty 0 24 | total_writeback 0 25 | total_swap 0 26 | total_pgpgin 56963 27 | total_pgpgout 53120 28 | total_pgfault 87136 29 | total_pgmajfault 149 30 | total_inactive_anon 8192 31 | total_active_anon 1482752 32 | total_inactive_file 19841024 33 | total_active_file 40439808 34 | total_unevictable 0 35 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v1/memory/memory.usage_in_bytes: -------------------------------------------------------------------------------- 1 | 69148672 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v1/memory_garbage/memory.limit_in_bytes: -------------------------------------------------------------------------------- 1 | 524288000 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v1/memory_garbage/memory.memsw.limit_in_bytes: -------------------------------------------------------------------------------- 1 | aaaaaaaaaa 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v1/memory_garbage/memory.memsw.usage_in_bytes: -------------------------------------------------------------------------------- 1 | 512000000 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v1/memory_garbage/memory.stat: -------------------------------------------------------------------------------- 1 | cache 60342272 2 | rss 1445888 3 | rss_huge 0 4 | mapped_file 3710976 5 | dirty 0 6 | writeback 0 7 | swap bbbbbbb 8 | pgpgin 56963 9 | pgpgout 53120 10 | pgfault 87136 11 | pgmajfault 149 12 | inactive_anon 8192 13 | active_anon 1482752 14 | inactive_file 19841024 15 | active_file 40439808 16 | unevictable 0 17 | hierarchical_memory_limit 524288000 18 | hierarchical_memsw_limit 1073741824 19 | total_cache 60342272 20 | total_rss 1445888 21 | total_rss_huge 0 22 | total_mapped_file 3710976 23 | total_dirty 0 24 | total_writeback 0 25 | total_swap 2048000 26 | total_pgpgin 56963 27 | total_pgpgout 53120 28 | total_pgfault 87136 29 | total_pgmajfault 149 30 | total_inactive_anon 8192 31 | total_active_anon 1482752 32 | total_inactive_file 19841024 33 | total_active_file 40439808 34 | total_unevictable 0 35 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v1/memory_garbage/memory.usage_in_bytes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appsignal/probes-rs/8862bb544127ae13317e62f80b2b12c3186ed89d/fixtures/linux/sys/fs/cgroup_v1/memory_garbage/memory.usage_in_bytes -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v1/memory_incomplete/memory.limit_in_bytes: -------------------------------------------------------------------------------- 1 | 524288000 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v1/memory_incomplete/memory.memsw.limit_in_bytes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appsignal/probes-rs/8862bb544127ae13317e62f80b2b12c3186ed89d/fixtures/linux/sys/fs/cgroup_v1/memory_incomplete/memory.memsw.limit_in_bytes -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v1/memory_incomplete/memory.memsw.usage_in_bytes: -------------------------------------------------------------------------------- 1 | 512000000 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v1/memory_incomplete/memory.stat: -------------------------------------------------------------------------------- 1 | cache 60342272 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v1/memory_incomplete/memory.usage_in_bytes: -------------------------------------------------------------------------------- 1 | bbbbbbbbbbbb 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v1/memory_missing_files/memory.stat: -------------------------------------------------------------------------------- 1 | cache 60342272 2 | rss 1445888 3 | rss_huge 0 4 | mapped_file 3710976 5 | dirty 0 6 | writeback 0 7 | swap 512000 8 | pgpgin 56963 9 | pgpgout 53120 10 | pgfault 87136 11 | pgmajfault 149 12 | inactive_anon 8192 13 | active_anon 1482752 14 | inactive_file 19841024 15 | active_file 40439808 16 | unevictable 0 17 | hierarchical_memory_limit 524288000 18 | hierarchical_memsw_limit 1073741824 19 | total_cache 60342272 20 | total_rss 1445888 21 | total_rss_huge 0 22 | total_mapped_file 3710976 23 | total_dirty 0 24 | total_writeback 0 25 | total_swap 2048000 26 | total_pgpgin 56963 27 | total_pgpgout 53120 28 | total_pgfault 87136 29 | total_pgmajfault 149 30 | total_inactive_anon 8192 31 | total_active_anon 1482752 32 | total_inactive_file 19841024 33 | total_active_file 40439808 34 | total_unevictable 0 35 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v1/memory_without_swap/memory.limit_in_bytes: -------------------------------------------------------------------------------- 1 | 524288000 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v1/memory_without_swap/memory.stat: -------------------------------------------------------------------------------- 1 | cache 60342272 2 | rss 1445888 3 | rss_huge 0 4 | mapped_file 3710976 5 | dirty 0 6 | writeback 0 7 | swap 0 8 | pgpgin 56963 9 | pgpgout 53120 10 | pgfault 87136 11 | pgmajfault 149 12 | inactive_anon 8192 13 | active_anon 1482752 14 | inactive_file 19841024 15 | active_file 40439808 16 | unevictable 0 17 | hierarchical_memory_limit 524288000 18 | hierarchical_memsw_limit 1073741824 19 | total_cache 60342272 20 | total_rss 1445888 21 | total_rss_huge 0 22 | total_mapped_file 3710976 23 | total_dirty 0 24 | total_writeback 0 25 | total_swap 0 26 | total_pgpgin 56963 27 | total_pgpgout 53120 28 | total_pgfault 87136 29 | total_pgmajfault 149 30 | total_inactive_anon 8192 31 | total_active_anon 1482752 32 | total_inactive_file 19841024 33 | total_active_file 40439808 34 | total_unevictable 0 35 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v1/memory_without_swap/memory.usage_in_bytes: -------------------------------------------------------------------------------- 1 | 69148672 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v2/cpu.max_2_cpus: -------------------------------------------------------------------------------- 1 | 200000 100000 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v2/cpu.max_default: -------------------------------------------------------------------------------- 1 | max 100000 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v2/cpu.max_garbage: -------------------------------------------------------------------------------- 1 | full garbage_2445 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v2/cpu.max_half: -------------------------------------------------------------------------------- 1 | 50000 100000 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v2/cpu.stat_1: -------------------------------------------------------------------------------- 1 | usage_usec 171462 2 | user_usec 53792 3 | system_usec 117670 4 | nr_periods 0 5 | nr_throttled 0 6 | throttled_usec 0 7 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v2/cpu.stat_2: -------------------------------------------------------------------------------- 1 | usage_usec 271417 2 | user_usec 71137 3 | system_usec 200279 4 | nr_periods 0 5 | nr_throttled 0 6 | throttled_usec 0 7 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v2/cpu.stat_garbage: -------------------------------------------------------------------------------- 1 | usage_usec aaaa 2 | user_usec bbbb 3 | system_usec cccc 4 | nr_periods dddd 5 | nr_throttled eeee 6 | throttled_usec ffff 7 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v2/cpu.stat_incomplete: -------------------------------------------------------------------------------- 1 | usage_usec 271417 2 | user_usec 71137 3 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v2/memory/memory.current: -------------------------------------------------------------------------------- 1 | 69148672 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v2/memory/memory.max: -------------------------------------------------------------------------------- 1 | 524288000 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v2/memory/memory.stat: -------------------------------------------------------------------------------- 1 | anon 0 2 | file 0 3 | kernel_stack 49152 4 | percpu 0 5 | sock 0 6 | shmem 0 7 | file_mapped 0 8 | file_dirty 0 9 | file_writeback 0 10 | anon_thp 0 11 | inactive_anon 0 12 | active_anon 0 13 | inactive_file 0 14 | active_file 0 15 | unevictable 0 16 | slab_reclaimable 0 17 | slab_unreclaimable 0 18 | slab 0 19 | workingset_refault_anon 0 20 | workingset_refault_file 0 21 | workingset_activate_anon 0 22 | workingset_activate_file 0 23 | workingset_restore_anon 0 24 | workingset_restore_file 0 25 | workingset_nodereclaim 0 26 | pgfault 1122 27 | pgmajfault 0 28 | pgrefill 0 29 | pgscan 0 30 | pgsteal 0 31 | pgactivate 0 32 | pgdeactivate 0 33 | pglazyfree 0 34 | pglazyfreed 0 35 | thp_fault_alloc 0 36 | thp_collapse_alloc 0 37 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v2/memory/memory.swap.current: -------------------------------------------------------------------------------- 1 | 512000000 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v2/memory/memory.swap.max: -------------------------------------------------------------------------------- 1 | 2048000000 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v2/memory_garbage/memory.current: -------------------------------------------------------------------------------- 1 | aaaaaaaa 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v2/memory_garbage/memory.max: -------------------------------------------------------------------------------- 1 | 524288000 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v2/memory_garbage/memory.stat: -------------------------------------------------------------------------------- 1 | anon 0 2 | file 0 3 | kernel_stack 49152 4 | percpu 0 5 | sock 0 6 | shmem 0 7 | file_mapped 0 8 | file_dirty 0 9 | file_writeback 0 10 | anon_thp 0 11 | inactive_anon 0 12 | active_anon 0 13 | inactive_file 0 14 | active_file 0 15 | unevictable 0 16 | slab_reclaimable 0 17 | slab_unreclaimable 0 18 | slab 0 19 | workingset_refault_anon 0 20 | workingset_refault_file 0 21 | workingset_activate_anon 0 22 | workingset_activate_file 0 23 | workingset_restore_anon 0 24 | workingset_restore_file 0 25 | workingset_nodereclaim 0 26 | pgfault 1122 27 | pgmajfault 0 28 | pgrefill 0 29 | pgscan 0 30 | pgsteal 0 31 | pgactivate 0 32 | pgdeactivate 0 33 | pglazyfree 0 34 | pglazyfreed 0 35 | thp_fault_alloc 0 36 | thp_collapse_alloc 0 37 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v2/memory_garbage/memory.swap.current: -------------------------------------------------------------------------------- 1 | 512000000 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v2/memory_garbage/memory.swap.max: -------------------------------------------------------------------------------- 1 | 2048000000 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v2/memory_incomplete/memory.current: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appsignal/probes-rs/8862bb544127ae13317e62f80b2b12c3186ed89d/fixtures/linux/sys/fs/cgroup_v2/memory_incomplete/memory.current -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v2/memory_incomplete/memory.max: -------------------------------------------------------------------------------- 1 | 524288000 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v2/memory_incomplete/memory.stat: -------------------------------------------------------------------------------- 1 | anon 0 2 | file 0 3 | kernel_stack 49152 4 | percpu 0 5 | sock 0 6 | shmem 0 7 | file_mapped 0 8 | file_dirty 0 9 | file_writeback 0 10 | anon_thp 0 11 | inactive_anon 0 12 | active_anon 0 13 | inactive_file 0 14 | active_file 0 15 | unevictable 0 16 | slab_reclaimable 0 17 | slab_unreclaimable 0 18 | slab 0 19 | workingset_refault_anon 0 20 | workingset_refault_file 0 21 | workingset_activate_anon 0 22 | workingset_activate_file 0 23 | workingset_restore_anon 0 24 | workingset_restore_file 0 25 | workingset_nodereclaim 0 26 | pgfault 1122 27 | pgmajfault 0 28 | pgrefill 0 29 | pgscan 0 30 | pgsteal 0 31 | pgactivate 0 32 | pgdeactivate 0 33 | pglazyfree 0 34 | pglazyfreed 0 35 | thp_fault_alloc 0 36 | thp_collapse_alloc 0 37 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v2/memory_incomplete/memory.swap.current: -------------------------------------------------------------------------------- 1 | 512000000 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v2/memory_incomplete/memory.swap.max: -------------------------------------------------------------------------------- 1 | 2048000000 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v2/memory_missing_files/memory.stat: -------------------------------------------------------------------------------- 1 | anon 0 2 | file 0 3 | kernel_stack 49152 4 | percpu 0 5 | sock 0 6 | shmem 0 7 | file_mapped 0 8 | file_dirty 0 9 | file_writeback 0 10 | anon_thp 0 11 | inactive_anon 0 12 | active_anon 0 13 | inactive_file 0 14 | active_file 0 15 | unevictable 0 16 | slab_reclaimable 0 17 | slab_unreclaimable 0 18 | slab 0 19 | workingset_refault_anon 0 20 | workingset_refault_file 0 21 | workingset_activate_anon 0 22 | workingset_activate_file 0 23 | workingset_restore_anon 0 24 | workingset_restore_file 0 25 | workingset_nodereclaim 0 26 | pgfault 1122 27 | pgmajfault 0 28 | pgrefill 0 29 | pgscan 0 30 | pgsteal 0 31 | pgactivate 0 32 | pgdeactivate 0 33 | pglazyfree 0 34 | pglazyfreed 0 35 | thp_fault_alloc 0 36 | thp_collapse_alloc 0 37 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v2/memory_without_swap/memory.current: -------------------------------------------------------------------------------- 1 | 69148672 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v2/memory_without_swap/memory.max: -------------------------------------------------------------------------------- 1 | 524288000 2 | -------------------------------------------------------------------------------- /fixtures/linux/sys/fs/cgroup_v2/memory_without_swap/memory.stat: -------------------------------------------------------------------------------- 1 | anon 0 2 | file 0 3 | kernel_stack 49152 4 | percpu 0 5 | sock 0 6 | shmem 0 7 | file_mapped 0 8 | file_dirty 0 9 | file_writeback 0 10 | anon_thp 0 11 | inactive_anon 0 12 | active_anon 0 13 | inactive_file 0 14 | active_file 0 15 | unevictable 0 16 | slab_reclaimable 0 17 | slab_unreclaimable 0 18 | slab 0 19 | workingset_refault_anon 0 20 | workingset_refault_file 0 21 | workingset_activate_anon 0 22 | workingset_activate_file 0 23 | workingset_restore_anon 0 24 | workingset_restore_file 0 25 | workingset_nodereclaim 0 26 | pgfault 1122 27 | pgmajfault 0 28 | pgrefill 0 29 | pgscan 0 30 | pgsteal 0 31 | pgactivate 0 32 | pgdeactivate 0 33 | pglazyfree 0 34 | pglazyfreed 0 35 | thp_fault_alloc 0 36 | thp_collapse_alloc 0 37 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | 1.72.0 2 | -------------------------------------------------------------------------------- /src/cpu/cgroup.rs: -------------------------------------------------------------------------------- 1 | use crate::error::ProbeError; 2 | use crate::{calculate_time_difference, dir_exists, time_adjusted, Result}; 3 | use std::path::Path; 4 | 5 | /// Measurement of cpu stats at a certain time 6 | #[derive(Debug, PartialEq)] 7 | pub struct CgroupCpuMeasurement { 8 | pub precise_time_ns: u64, 9 | pub stat: CgroupCpuStat, 10 | } 11 | 12 | impl CgroupCpuMeasurement { 13 | pub fn calculate_per_minute( 14 | &self, 15 | next_measurement: &CgroupCpuMeasurement, 16 | ) -> Result { 17 | let time_difference = 18 | calculate_time_difference(self.precise_time_ns, next_measurement.precise_time_ns)?; 19 | 20 | Ok(CgroupCpuStat { 21 | total_usage: time_adjusted( 22 | "total_usage", 23 | next_measurement.stat.total_usage, 24 | self.stat.total_usage, 25 | time_difference, 26 | )?, 27 | user: time_adjusted( 28 | "user", 29 | next_measurement.stat.user, 30 | self.stat.user, 31 | time_difference, 32 | )?, 33 | system: time_adjusted( 34 | "system", 35 | next_measurement.stat.system, 36 | self.stat.system, 37 | time_difference, 38 | )?, 39 | }) 40 | } 41 | } 42 | 43 | /// Container CPU stats for a minute 44 | #[derive(Debug, PartialEq)] 45 | pub struct CgroupCpuStat { 46 | pub total_usage: u64, 47 | pub user: u64, 48 | pub system: u64, 49 | } 50 | 51 | impl CgroupCpuStat { 52 | /// Calculate the weight of the various components in percentages 53 | pub fn in_percentages(&self) -> CgroupCpuStatPercentages { 54 | CgroupCpuStatPercentages { 55 | total_usage: self.percentage_of_total(self.total_usage), 56 | user: self.percentage_of_total(self.user), 57 | system: self.percentage_of_total(self.system), 58 | } 59 | } 60 | 61 | // Divide the values by the number of (potentially fractional) CPUs allocated to the system. 62 | pub fn by_cpu_count(&self, cpu_count: Option) -> CgroupCpuStat { 63 | let cpu_count = cpu_count.filter(|count| *count != 0.0).unwrap_or(1.0); 64 | 65 | CgroupCpuStat { 66 | total_usage: (self.total_usage as f64 / cpu_count).round() as u64, 67 | user: (self.user as f64 / cpu_count).round() as u64, 68 | system: (self.system as f64 / cpu_count).round() as u64, 69 | } 70 | } 71 | 72 | fn percentage_of_total(&self, value: u64) -> f32 { 73 | // 60_000_000_000 being the total value. This is 60 seconds expressed in nanoseconds. 74 | (value as f32 / 60_000_000_000.0) * 100.0 75 | } 76 | } 77 | 78 | /// Cgroup Cpu stats converted to percentages 79 | #[derive(Debug, PartialEq)] 80 | pub struct CgroupCpuStatPercentages { 81 | pub total_usage: f32, 82 | pub user: f32, 83 | pub system: f32, 84 | } 85 | 86 | /// Read the current CPU stats of the container. 87 | #[cfg(target_os = "linux")] 88 | pub fn read(cpu_count: Option) -> Result { 89 | use super::cgroup_v1::read_and_parse_v1_sys_stat; 90 | use super::cgroup_v2::read_and_parse_v2_sys_stat; 91 | 92 | let v2_sys_fs_file = Path::new("/sys/fs/cgroup/cpu.stat"); 93 | if v2_sys_fs_file.exists() { 94 | let v2_sys_fs_cpu_max_file = Path::new("/sys/fs/cgroup/cpu.max"); 95 | return read_and_parse_v2_sys_stat(&v2_sys_fs_file, v2_sys_fs_cpu_max_file, cpu_count); 96 | } 97 | 98 | let v1_sys_fs_dir = Path::new("/sys/fs/cgroup/cpuacct/"); 99 | if dir_exists(v1_sys_fs_dir) { 100 | return read_and_parse_v1_sys_stat( 101 | &v1_sys_fs_dir, 102 | &Path::new("/sys/fs/cgroup/cpu/cpu.cfs_period_us"), 103 | &Path::new("/sys/fs/cgroup/cpu/cpu.cfs_quota_us"), 104 | cpu_count, 105 | ); 106 | } 107 | 108 | Err(ProbeError::UnexpectedContent(format!( 109 | "Directory `{}` and file `{}` not found", 110 | v1_sys_fs_dir.to_str().unwrap_or("unknown path"), 111 | v2_sys_fs_file.to_str().unwrap_or("unknown path") 112 | ))) 113 | } 114 | 115 | #[cfg(test)] 116 | #[cfg(target_os = "linux")] 117 | mod test { 118 | use super::{CgroupCpuMeasurement, CgroupCpuStat}; 119 | use crate::error::ProbeError; 120 | 121 | #[test] 122 | fn test_read() { 123 | assert!(super::read(None).is_ok()); 124 | assert!(super::read(Some(0.5)).is_ok()); 125 | } 126 | 127 | #[test] 128 | fn test_calculate_per_minute_wrong_times() { 129 | let measurement1 = CgroupCpuMeasurement { 130 | precise_time_ns: 90_000_000_000, 131 | stat: CgroupCpuStat { 132 | total_usage: 0, 133 | user: 0, 134 | system: 0, 135 | }, 136 | }; 137 | 138 | let measurement2 = CgroupCpuMeasurement { 139 | precise_time_ns: 60_000_000_000, 140 | stat: CgroupCpuStat { 141 | total_usage: 0, 142 | user: 0, 143 | system: 0, 144 | }, 145 | }; 146 | 147 | match measurement1.calculate_per_minute(&measurement2) { 148 | Err(ProbeError::InvalidInput(_)) => (), 149 | r => panic!("Unexpected result: {:?}", r), 150 | } 151 | } 152 | 153 | #[test] 154 | fn test_cgroup_calculate_per_minute_full_minute() { 155 | let measurement1 = CgroupCpuMeasurement { 156 | precise_time_ns: 60_000_000_000, 157 | stat: CgroupCpuStat { 158 | total_usage: 6380, 159 | user: 1000, 160 | system: 1200, 161 | }, 162 | }; 163 | 164 | let measurement2 = CgroupCpuMeasurement { 165 | precise_time_ns: 120_000_000_000, 166 | stat: CgroupCpuStat { 167 | total_usage: 6440, 168 | user: 1006, 169 | system: 1206, 170 | }, 171 | }; 172 | 173 | let expected = CgroupCpuStat { 174 | total_usage: 60, 175 | user: 6, 176 | system: 6, 177 | }; 178 | 179 | let stat = measurement1.calculate_per_minute(&measurement2).unwrap(); 180 | 181 | assert_eq!(stat, expected); 182 | } 183 | 184 | #[test] 185 | fn test_calculate_per_minute_partial_minute() { 186 | let measurement1 = CgroupCpuMeasurement { 187 | precise_time_ns: 60_000_000_000, 188 | stat: CgroupCpuStat { 189 | total_usage: 1_000_000_000, 190 | user: 10000_000_000, 191 | system: 12000_000_000, 192 | }, 193 | }; 194 | 195 | let measurement2 = CgroupCpuMeasurement { 196 | precise_time_ns: 90_000_000_000, 197 | stat: CgroupCpuStat { 198 | total_usage: 1_500_000_000, 199 | user: 10060_000_000, 200 | system: 12060_000_000, 201 | }, 202 | }; 203 | 204 | let expected = CgroupCpuStat { 205 | total_usage: 1_000_000_000, 206 | user: 120_000_000, 207 | system: 120_000_000, 208 | }; 209 | 210 | let stat = measurement1.calculate_per_minute(&measurement2).unwrap(); 211 | 212 | assert_eq!(stat, expected); 213 | } 214 | 215 | #[test] 216 | fn test_calculate_per_minute_values_lower() { 217 | let measurement1 = CgroupCpuMeasurement { 218 | precise_time_ns: 60_000_000_000, 219 | stat: CgroupCpuStat { 220 | total_usage: 63800_000_000, 221 | user: 10000_000_000, 222 | system: 12000_000_000, 223 | }, 224 | }; 225 | 226 | let measurement2 = CgroupCpuMeasurement { 227 | precise_time_ns: 90_000_000_000, 228 | stat: CgroupCpuStat { 229 | total_usage: 10400_000_000, 230 | user: 1060_000_000, 231 | system: 1260_000_000, 232 | }, 233 | }; 234 | 235 | match measurement1.calculate_per_minute(&measurement2) { 236 | Err(ProbeError::UnexpectedContent(_)) => (), 237 | r => panic!("Unexpected result: {:?}", r), 238 | } 239 | } 240 | 241 | #[test] 242 | fn test_in_percentages() { 243 | let stat = CgroupCpuStat { 244 | total_usage: 24000000000, 245 | user: 16800000000, 246 | system: 1200000000, 247 | }; 248 | 249 | let in_percentages = stat.in_percentages(); 250 | 251 | // Rounding in the floating point calculations can vary, so check if this 252 | // is in the correct range. 253 | assert!(in_percentages.total_usage > 39.9); 254 | assert!(in_percentages.total_usage <= 40.0); 255 | 256 | assert!(in_percentages.user > 27.9); 257 | assert!(in_percentages.user <= 28.0); 258 | 259 | assert!(in_percentages.system > 1.9); 260 | assert!(in_percentages.system <= 2.0); 261 | } 262 | 263 | #[test] 264 | fn test_in_percentages_fractions() { 265 | let stat = CgroupCpuStat { 266 | total_usage: 24000000000, 267 | user: 17100000000, 268 | system: 900000000, 269 | }; 270 | 271 | let in_percentages = stat.in_percentages(); 272 | 273 | // Rounding in the floating point calculations can vary, so check if this 274 | // is in the correct range. 275 | assert!(in_percentages.total_usage > 39.9); 276 | assert!(in_percentages.total_usage <= 40.0); 277 | 278 | assert!(in_percentages.user > 28.4); 279 | assert!(in_percentages.user <= 28.5); 280 | 281 | assert!(in_percentages.system > 1.4); 282 | assert!(in_percentages.system <= 1.5); 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /src/cpu/cgroup_v1.rs: -------------------------------------------------------------------------------- 1 | use super::cgroup::{CgroupCpuMeasurement, CgroupCpuStat}; 2 | use crate::error::ProbeError; 3 | use crate::{ 4 | file_to_buf_reader, file_to_string, parse_u64, path_to_string, precise_time_ns, 5 | read_file_value_as_u64, Result, 6 | }; 7 | use std::io::BufRead; 8 | use std::path::Path; 9 | 10 | const CPU_SYS_V1_NUMBER_OF_FIELDS: usize = 2; 11 | 12 | #[cfg(target_os = "linux")] 13 | pub fn read_and_parse_v1_sys_stat( 14 | path: &Path, 15 | cpu_period_path: &Path, 16 | cpu_quota_path: &Path, 17 | mut cpu_count: Option, 18 | ) -> Result { 19 | let time = precise_time_ns(); 20 | 21 | if cpu_count.is_none() { 22 | // If the CPU period and quota files exist, we can use it to calculate the number of CPUs in 23 | // the cgroup. 24 | if cpu_period_path.exists() && cpu_quota_path.exists() { 25 | let cpu_period = parse_u64(file_to_string(&cpu_period_path)?.trim())? as f64; 26 | let cpu_quota_raw = file_to_string(&cpu_quota_path)?.trim().to_string(); 27 | // The value `-1` means no quota is set and we can't calculate the number of CPUs present. 28 | if cpu_quota_raw != "-1" { 29 | let cpu_quota = parse_u64(&cpu_quota_raw)? as f64; 30 | cpu_count = Some(cpu_quota / cpu_period); 31 | } 32 | } 33 | }; 34 | 35 | let reader = file_to_buf_reader(&path.join("cpuacct.stat"))?; 36 | let total_usage = read_file_value_as_u64(&path.join("cpuacct.usage"))?; 37 | 38 | let mut cpu = CgroupCpuStat { 39 | total_usage, 40 | user: 0, 41 | system: 0, 42 | }; 43 | 44 | let mut fields_encountered = 0; 45 | for line in reader.lines() { 46 | let line = line.map_err(|e| ProbeError::IO(e, path_to_string(path)))?; 47 | let segments: Vec<&str> = line.split_whitespace().collect(); 48 | let value = parse_u64(&segments[1])?; 49 | fields_encountered += match segments[0] { 50 | "user" => { 51 | cpu.user = value * 10_000_000; 52 | 1 53 | } 54 | "system" => { 55 | cpu.system = value * 10_000_000; 56 | 1 57 | } 58 | _ => 0, 59 | }; 60 | 61 | if fields_encountered == CPU_SYS_V1_NUMBER_OF_FIELDS { 62 | break; 63 | } 64 | } 65 | 66 | if fields_encountered != CPU_SYS_V1_NUMBER_OF_FIELDS { 67 | return Err(ProbeError::UnexpectedContent( 68 | "Did not encounter all expected fields".to_owned(), 69 | )); 70 | } 71 | let measurement = CgroupCpuMeasurement { 72 | precise_time_ns: time, 73 | stat: cpu.by_cpu_count(cpu_count), 74 | }; 75 | Ok(measurement) 76 | } 77 | 78 | #[cfg(test)] 79 | #[cfg(target_os = "linux")] 80 | mod test { 81 | use super::read_and_parse_v1_sys_stat; 82 | use crate::error::ProbeError; 83 | use std::path::Path; 84 | 85 | #[test] 86 | fn test_read_v1_sys_measurement_no_quota() { 87 | let measurement = read_and_parse_v1_sys_stat( 88 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpuacct_1/"), 89 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/does_not_exist"), 90 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/does_not_exist"), 91 | None, 92 | ) 93 | .unwrap(); 94 | let cpu = measurement.stat; 95 | assert_eq!(cpu.total_usage, 152657213021); 96 | assert_eq!(cpu.user, 149340000000); 97 | assert_eq!(cpu.system, 980000000); 98 | } 99 | 100 | #[test] 101 | fn test_read_v1_sys_measurement_one_cpu() { 102 | let measurement = read_and_parse_v1_sys_stat( 103 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpuacct_1/"), 104 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/cpu.cfs_period_us"), 105 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/cpu.cfs_quota_us.one_cpu"), 106 | None, 107 | ) 108 | .unwrap(); 109 | let cpu = measurement.stat; 110 | assert_eq!(cpu.total_usage, 152657213021); 111 | assert_eq!(cpu.user, 149340000000); 112 | assert_eq!(cpu.system, 980000000); 113 | } 114 | 115 | #[test] 116 | fn test_read_v1_sys_measurement_two_cpu() { 117 | let measurement = read_and_parse_v1_sys_stat( 118 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpuacct_1/"), 119 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/cpu.cfs_period_us"), 120 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/cpu.cfs_quota_us.two_cpu"), 121 | None, 122 | ) 123 | .unwrap(); 124 | let cpu = measurement.stat; 125 | assert_eq!(cpu.total_usage, 76328606511); 126 | assert_eq!(cpu.user, 74670000000); 127 | assert_eq!(cpu.system, 490000000); 128 | } 129 | 130 | #[test] 131 | fn test_read_v1_sys_measurement_half_cpu() { 132 | let measurement = read_and_parse_v1_sys_stat( 133 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpuacct_1/"), 134 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/cpu.cfs_period_us"), 135 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/cpu.cfs_quota_us.half_cpu"), 136 | None, 137 | ) 138 | .unwrap(); 139 | let cpu = measurement.stat; 140 | assert_eq!(cpu.total_usage, 305314426042); 141 | assert_eq!(cpu.user, 298680000000); 142 | assert_eq!(cpu.system, 1960000000); 143 | } 144 | 145 | #[test] 146 | fn test_read_v1_sys_measurement_minus_one() { 147 | let measurement = read_and_parse_v1_sys_stat( 148 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpuacct_1/"), 149 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/cpu.cfs_period_us"), 150 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/cpu.cfs_quota_us.minus_one"), 151 | None, 152 | ) 153 | .unwrap(); 154 | let cpu = measurement.stat; 155 | // Does not divide by the number of CPUs 156 | assert_eq!(cpu.total_usage, 152657213021); 157 | assert_eq!(cpu.user, 149340000000); 158 | assert_eq!(cpu.system, 980000000); 159 | } 160 | 161 | #[test] 162 | fn test_read_v1_sys_measurement_one_cpu_count() { 163 | let measurement = read_and_parse_v1_sys_stat( 164 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpuacct_1/"), 165 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/cpu.cfs_period_us"), 166 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/cpu.cfs_quota_us.minus_one"), 167 | Some(1.0), 168 | ) 169 | .unwrap(); 170 | let cpu = measurement.stat; 171 | assert_eq!(cpu.total_usage, 152657213021); 172 | assert_eq!(cpu.user, 149340000000); 173 | assert_eq!(cpu.system, 980000000); 174 | } 175 | 176 | #[test] 177 | fn test_read_v1_sys_measurement_half_cpu_count() { 178 | let measurement = read_and_parse_v1_sys_stat( 179 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpuacct_1/"), 180 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/cpu.cfs_period_us"), 181 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/cpu.cfs_quota_us.minus_one"), 182 | Some(0.5), 183 | ) 184 | .unwrap(); 185 | let cpu = measurement.stat; 186 | assert_eq!(cpu.total_usage, 305314426042); 187 | assert_eq!(cpu.user, 298680000000); 188 | assert_eq!(cpu.system, 1960000000); 189 | } 190 | 191 | #[test] 192 | fn test_read_v1_sys_measurement_two_cpu_count() { 193 | let measurement = read_and_parse_v1_sys_stat( 194 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpuacct_1/"), 195 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/cpu.cfs_period_us"), 196 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/cpu.cfs_quota_us.minus_one"), 197 | Some(2.0), 198 | ) 199 | .unwrap(); 200 | let cpu = measurement.stat; 201 | assert_eq!(cpu.total_usage, 76328606511); 202 | assert_eq!(cpu.user, 74670000000); 203 | assert_eq!(cpu.system, 490000000); 204 | } 205 | 206 | #[test] 207 | fn test_read_v1_sys_wrong_path() { 208 | match read_and_parse_v1_sys_stat( 209 | &Path::new("bananas"), 210 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/does_not_exist"), 211 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/does_not_exist"), 212 | None, 213 | ) { 214 | Err(ProbeError::IO(_, _)) => (), 215 | r => panic!("Unexpected result: {:?}", r), 216 | } 217 | } 218 | 219 | #[test] 220 | fn test_read_and_parse_v1_sys_stat_incomplete() { 221 | match read_and_parse_v1_sys_stat( 222 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpuacct_incomplete/"), 223 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/does_not_exist"), 224 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/does_not_exist"), 225 | None, 226 | ) { 227 | Err(ProbeError::UnexpectedContent(_)) => (), 228 | r => panic!("Unexpected result: {:?}", r), 229 | } 230 | } 231 | 232 | #[test] 233 | fn test_read_and_parse_v1_sys_stat_garbage() { 234 | match read_and_parse_v1_sys_stat( 235 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpuacct_garbage/"), 236 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/does_not_exist"), 237 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/does_not_exist"), 238 | None, 239 | ) { 240 | Err(ProbeError::UnexpectedContent(_)) => (), 241 | r => panic!("Unexpected result: {:?}", r), 242 | } 243 | } 244 | 245 | #[test] 246 | fn test_in_percentages_integration_v1() { 247 | let mut measurement1 = read_and_parse_v1_sys_stat( 248 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpuacct_1/"), 249 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/does_not_exist"), 250 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/does_not_exist"), 251 | None, 252 | ) 253 | .unwrap(); 254 | measurement1.precise_time_ns = 375953965125920; 255 | let mut measurement2 = read_and_parse_v1_sys_stat( 256 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpuacct_2/"), 257 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/does_not_exist"), 258 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/does_not_exist"), 259 | None, 260 | ) 261 | .unwrap(); 262 | measurement2.precise_time_ns = 376013815302920; 263 | 264 | let stat = measurement1.calculate_per_minute(&measurement2).unwrap(); 265 | let in_percentages = stat.in_percentages(); 266 | 267 | // Rounding in the floating point calculations can vary, so check if this 268 | // is in the correct range. 269 | assert!(in_percentages.total_usage > 49.70); 270 | assert!(in_percentages.total_usage < 49.71); 271 | 272 | assert!(in_percentages.user > 47.60); 273 | assert!(in_percentages.user < 47.61); 274 | 275 | assert!(in_percentages.system > 0.38); 276 | assert!(in_percentages.system < 0.39); 277 | } 278 | 279 | #[test] 280 | fn test_in_percentages_integration_two_cpu() { 281 | let mut measurement1 = read_and_parse_v1_sys_stat( 282 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpuacct_1/"), 283 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/cpu.cfs_period_us"), 284 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/cpu.cfs_quota_us.two_cpu"), 285 | None, 286 | ) 287 | .unwrap(); 288 | measurement1.precise_time_ns = 375953965125920; 289 | let mut measurement2 = read_and_parse_v1_sys_stat( 290 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpuacct_2/"), 291 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/cpu.cfs_period_us"), 292 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/cpu.cfs_quota_us.two_cpu"), 293 | None, 294 | ) 295 | .unwrap(); 296 | measurement2.precise_time_ns = 376013815302920; 297 | 298 | let stat = measurement1.calculate_per_minute(&measurement2).unwrap(); 299 | let in_percentages = stat.in_percentages(); 300 | 301 | // Rounding in the floating point calculations can vary, so check if this 302 | // is in the correct range. 303 | assert!(in_percentages.total_usage > 24.85); 304 | assert!(in_percentages.total_usage < 24.86); 305 | 306 | assert!(in_percentages.user > 23.80); 307 | assert!(in_percentages.user < 23.81); 308 | 309 | assert!(in_percentages.system > 0.19); 310 | assert!(in_percentages.system < 0.20); 311 | } 312 | 313 | #[test] 314 | fn test_in_percentages_integration_half_cpu() { 315 | let mut measurement1 = read_and_parse_v1_sys_stat( 316 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpuacct_1/"), 317 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/cpu.cfs_period_us"), 318 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/cpu.cfs_quota_us.half_cpu"), 319 | None, 320 | ) 321 | .unwrap(); 322 | measurement1.precise_time_ns = 375953965125920; 323 | let mut measurement2 = read_and_parse_v1_sys_stat( 324 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpuacct_2/"), 325 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/cpu.cfs_period_us"), 326 | &Path::new("fixtures/linux/sys/fs/cgroup_v1/cpu_quota/cpu.cfs_quota_us.half_cpu"), 327 | None, 328 | ) 329 | .unwrap(); 330 | measurement2.precise_time_ns = 376013815302920; 331 | 332 | let stat = measurement1.calculate_per_minute(&measurement2).unwrap(); 333 | let in_percentages = stat.in_percentages(); 334 | 335 | // Rounding in the floating point calculations can vary, so check if this 336 | // is in the correct range. 337 | assert!(in_percentages.total_usage > 99.40); 338 | assert!(in_percentages.total_usage < 99.41); 339 | 340 | assert!(in_percentages.user > 95.20); 341 | assert!(in_percentages.user < 95.21); 342 | 343 | assert!(in_percentages.system > 0.76); 344 | assert!(in_percentages.system < 0.77); 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /src/cpu/cgroup_v2.rs: -------------------------------------------------------------------------------- 1 | use super::cgroup::{CgroupCpuMeasurement, CgroupCpuStat}; 2 | use crate::error::ProbeError; 3 | use crate::{file_to_buf_reader, parse_u64, path_to_string, precise_time_ns, Result}; 4 | use std::io::BufRead; 5 | use std::path::Path; 6 | 7 | const CPU_SYS_V2_NUMBER_OF_FIELDS: usize = 3; 8 | 9 | #[cfg(target_os = "linux")] 10 | pub fn read_and_parse_v2_sys_stat( 11 | path: &Path, 12 | cpu_max_path: &Path, 13 | mut cpu_count: Option, 14 | ) -> Result { 15 | // If the cpu.max file exists, we can use it to calculate the number of CPUs 16 | // in the cgroup. It's also required that the first value is not set to "max", 17 | // otherwise we can't calculate the number of CPUs. 18 | if cpu_count.is_none() { 19 | if cpu_max_path.exists() { 20 | let reader = file_to_buf_reader(&cpu_max_path)?; 21 | let mut lines = reader.lines(); 22 | if let Some(Ok(line)) = lines.next() { 23 | let segments: Vec<&str> = line.split_whitespace().collect(); 24 | let max = segments[0]; 25 | 26 | if max != "max" { 27 | let period = parse_u64(&segments[1])? as f64; 28 | cpu_count = Some(parse_u64(&max)? as f64 / period); 29 | } 30 | } 31 | } 32 | } 33 | 34 | let time = precise_time_ns(); 35 | let reader = file_to_buf_reader(&path)?; 36 | 37 | let mut cpu = CgroupCpuStat { 38 | total_usage: 0, 39 | user: 0, 40 | system: 0, 41 | }; 42 | 43 | let mut fields_encountered = 0; 44 | for line in reader.lines() { 45 | let line = line.map_err(|e| ProbeError::IO(e, path_to_string(path)))?; 46 | let segments: Vec<&str> = line.split_whitespace().collect(); 47 | let value = parse_u64(&segments[1])?; 48 | fields_encountered += match segments[0] { 49 | "usage_usec" => { 50 | cpu.total_usage = value * 1_000; 51 | 1 52 | } 53 | "user_usec" => { 54 | cpu.user = value * 1_000; 55 | 1 56 | } 57 | "system_usec" => { 58 | cpu.system = value * 1_000; 59 | 1 60 | } 61 | _ => 0, 62 | }; 63 | 64 | if fields_encountered == CPU_SYS_V2_NUMBER_OF_FIELDS { 65 | break; 66 | } 67 | } 68 | 69 | if fields_encountered != CPU_SYS_V2_NUMBER_OF_FIELDS { 70 | return Err(ProbeError::UnexpectedContent( 71 | "Did not encounter all expected fields".to_owned(), 72 | )); 73 | } 74 | let measurement = CgroupCpuMeasurement { 75 | precise_time_ns: time, 76 | stat: cpu.by_cpu_count(cpu_count), 77 | }; 78 | Ok(measurement) 79 | } 80 | 81 | #[cfg(test)] 82 | #[cfg(target_os = "linux")] 83 | mod test { 84 | use super::read_and_parse_v2_sys_stat; 85 | use crate::error::ProbeError; 86 | use std::{option::Option::None, path::Path}; 87 | 88 | #[test] 89 | fn test_read_v2_sys_measurement_default_cpu_max() { 90 | let measurement = read_and_parse_v2_sys_stat( 91 | &Path::new("fixtures/linux/sys/fs/cgroup_v2/cpu.stat_1"), 92 | &Path::new("fixtures/linux/sys/fs/cgroup_v2/cpu.max_default"), 93 | None, 94 | ) 95 | .unwrap(); 96 | let cpu = measurement.stat; 97 | assert_eq!(cpu.total_usage, 171462000); 98 | assert_eq!(cpu.user, 53792000); 99 | assert_eq!(cpu.system, 117670000); 100 | } 101 | 102 | #[test] 103 | fn test_read_v2_sys_measurement_2_cpus() { 104 | let measurement = read_and_parse_v2_sys_stat( 105 | &Path::new("fixtures/linux/sys/fs/cgroup_v2/cpu.stat_1"), 106 | &Path::new("fixtures/linux/sys/fs/cgroup_v2/cpu.max_2_cpus"), 107 | None, 108 | ) 109 | .unwrap(); 110 | let cpu = measurement.stat; 111 | assert_eq!(cpu.total_usage, 85731000); 112 | assert_eq!(cpu.user, 26896000); 113 | assert_eq!(cpu.system, 58835000); 114 | } 115 | 116 | #[test] 117 | fn test_read_v2_sys_measurement_half_usage() { 118 | let measurement = read_and_parse_v2_sys_stat( 119 | &Path::new("fixtures/linux/sys/fs/cgroup_v2/cpu.stat_1"), 120 | &Path::new("fixtures/linux/sys/fs/cgroup_v2/cpu.max_half"), 121 | None, 122 | ) 123 | .unwrap(); 124 | let cpu = measurement.stat; 125 | assert_eq!(cpu.total_usage, 342924000); 126 | assert_eq!(cpu.user, 107584000); 127 | assert_eq!(cpu.system, 235340000); 128 | } 129 | 130 | #[test] 131 | fn test_read_v2_sys_one_cpu_count() { 132 | let measurement = read_and_parse_v2_sys_stat( 133 | &Path::new("fixtures/linux/sys/fs/cgroup_v2/cpu.stat_1"), 134 | &Path::new("fixtures/linux/sys/fs/cgroup_v2/cpu.max_garbage"), 135 | Some(1.0), 136 | ) 137 | .unwrap(); 138 | let cpu = measurement.stat; 139 | assert_eq!(cpu.total_usage, 171462000); 140 | assert_eq!(cpu.user, 53792000); 141 | assert_eq!(cpu.system, 117670000); 142 | } 143 | 144 | #[test] 145 | fn test_read_v2_sys_measurement_two_cpu_count() { 146 | let measurement = read_and_parse_v2_sys_stat( 147 | &Path::new("fixtures/linux/sys/fs/cgroup_v2/cpu.stat_1"), 148 | &Path::new("fixtures/linux/sys/fs/cgroup_v2/cpu.max_garbage"), 149 | Some(2.0), 150 | ) 151 | .unwrap(); 152 | let cpu = measurement.stat; 153 | assert_eq!(cpu.total_usage, 85731000); 154 | assert_eq!(cpu.user, 26896000); 155 | assert_eq!(cpu.system, 58835000); 156 | } 157 | 158 | #[test] 159 | fn test_read_v2_sys_measurement_half_cpu_count() { 160 | let measurement = read_and_parse_v2_sys_stat( 161 | &Path::new("fixtures/linux/sys/fs/cgroup_v2/cpu.stat_1"), 162 | &Path::new("fixtures/linux/sys/fs/cgroup_v2/cpu.max_garbage"), 163 | Some(0.5), 164 | ) 165 | .unwrap(); 166 | let cpu = measurement.stat; 167 | assert_eq!(cpu.total_usage, 342924000); 168 | assert_eq!(cpu.user, 107584000); 169 | assert_eq!(cpu.system, 235340000); 170 | } 171 | 172 | #[test] 173 | fn test_read_v2_sys_wrong_path() { 174 | match read_and_parse_v2_sys_stat(&Path::new("bananas"), &Path::new("potato"), None) { 175 | Err(ProbeError::IO(_, _)) => (), 176 | r => panic!("Unexpected result: {:?}", r), 177 | } 178 | } 179 | 180 | #[test] 181 | fn test_read_and_parse_v2_sys_stat_incomplete() { 182 | match read_and_parse_v2_sys_stat( 183 | &Path::new("fixtures/linux/sys/fs/cgroup_v2/cpu.stat_incomplete"), 184 | &Path::new("fixtures/linux/sys/fs/cgroup_v2/cpu.max_default"), 185 | None, 186 | ) { 187 | Err(ProbeError::UnexpectedContent(_)) => (), 188 | r => panic!("Unexpected result: {:?}", r), 189 | } 190 | } 191 | 192 | #[test] 193 | fn test_read_and_parse_v2_sys_stat_garbage() { 194 | let path = Path::new("fixtures/linux/sys/fs/cgroup_v2/cpu.stat_garbage"); 195 | let max_file_path = Path::new("fixtures/linux/fs/cgroup_v2/cpu.max"); 196 | match read_and_parse_v2_sys_stat(&path, &max_file_path, None) { 197 | Err(ProbeError::UnexpectedContent(_)) => (), 198 | r => panic!("Unexpected result: {:?}", r), 199 | } 200 | } 201 | 202 | #[test] 203 | fn test_read_and_parse_v2_sys_max_garbage() { 204 | let path = Path::new("fixtures/linux/sys/fs/cgroup_v2/cpu.stat_1"); 205 | let max_file_path = Path::new("fixtures/linux/sys/fs/cgroup_v2/cpu.max_garbage"); 206 | match read_and_parse_v2_sys_stat(&path, &max_file_path, None) { 207 | Err(ProbeError::UnexpectedContent(_)) => (), 208 | r => panic!("Unexpected result: {:?}", r), 209 | } 210 | } 211 | 212 | #[test] 213 | fn test_in_percentages_integration_v2_two_cpu() { 214 | let mut measurement1 = read_and_parse_v2_sys_stat( 215 | &Path::new("fixtures/linux/sys/fs/cgroup_v2/cpu.stat_1"), 216 | &Path::new("fixtures/linux/sys/fs/cgroup_v2/cpu.max_2_cpus"), 217 | None, 218 | ) 219 | .unwrap(); 220 | measurement1.precise_time_ns = 375953965125920; 221 | let mut measurement2 = read_and_parse_v2_sys_stat( 222 | &Path::new("fixtures/linux/sys/fs/cgroup_v2/cpu.stat_2"), 223 | &Path::new("fixtures/linux/sys/fs/cgroup_v2/cpu.max_2_cpus"), 224 | None, 225 | ) 226 | .unwrap(); 227 | measurement2.precise_time_ns = 376013815302920; 228 | 229 | let stat = measurement1.calculate_per_minute(&measurement2).unwrap(); 230 | let in_percentages = stat.in_percentages(); 231 | 232 | // Rounding in the floating point calculations can vary, so check if this 233 | // is in the correct range. 234 | assert!(in_percentages.total_usage > 0.08); 235 | assert!(in_percentages.total_usage < 0.09); 236 | 237 | assert!(in_percentages.user > 0.01); 238 | assert!(in_percentages.user < 0.02); 239 | 240 | assert!(in_percentages.system > 0.06); 241 | assert!(in_percentages.system < 0.07); 242 | } 243 | 244 | // When the cpu.max file does not return an integer. 245 | #[test] 246 | fn test_in_percentages_integration_v2_half_cpu() { 247 | let mut measurement1 = read_and_parse_v2_sys_stat( 248 | &Path::new("fixtures/linux/sys/fs/cgroup_v2/cpu.stat_1"), 249 | &Path::new("fixtures/linux/sys/fs/cgroup_v2/cpu.max_half"), 250 | None, 251 | ) 252 | .unwrap(); 253 | measurement1.precise_time_ns = 375953965125920; 254 | let mut measurement2 = read_and_parse_v2_sys_stat( 255 | &Path::new("fixtures/linux/sys/fs/cgroup_v2/cpu.stat_2"), 256 | &Path::new("fixtures/linux/sys/fs/cgroup_v2/cpu.max_half"), 257 | None, 258 | ) 259 | .unwrap(); 260 | measurement2.precise_time_ns = 376013815302920; 261 | 262 | let stat = measurement1.calculate_per_minute(&measurement2).unwrap(); 263 | let in_percentages = stat.in_percentages(); 264 | 265 | // Rounding in the floating point calculations can vary, so check if this 266 | // is in the correct range. 267 | assert!(in_percentages.total_usage > 0.33); 268 | assert!(in_percentages.total_usage < 0.34); 269 | 270 | assert!(in_percentages.user > 0.05); 271 | assert!(in_percentages.user < 0.06); 272 | 273 | assert!(in_percentages.system > 0.27); 274 | assert!(in_percentages.system < 0.28); 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /src/cpu/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cgroup; 2 | mod cgroup_v1; 3 | mod cgroup_v2; 4 | pub mod proc; 5 | -------------------------------------------------------------------------------- /src/cpu/proc.rs: -------------------------------------------------------------------------------- 1 | use super::super::{calculate_time_difference, time_adjusted, Result}; 2 | 3 | /// Measurement of cpu stats at a certain time 4 | #[derive(Debug, PartialEq)] 5 | pub struct CpuMeasurement { 6 | pub precise_time_ns: u64, 7 | pub stat: CpuStat, 8 | } 9 | 10 | impl CpuMeasurement { 11 | /// Calculate the cpu stats based on this measurement and a measurement in the future. 12 | /// It is advisable to make the next measurement roughly a minute from this one for the 13 | /// most reliable result. 14 | pub fn calculate_per_minute(&self, next_measurement: &CpuMeasurement) -> Result { 15 | let time_difference = 16 | calculate_time_difference(self.precise_time_ns, next_measurement.precise_time_ns)?; 17 | 18 | Ok(CpuStat { 19 | total: time_adjusted( 20 | "total", 21 | next_measurement.stat.total, 22 | self.stat.total, 23 | time_difference, 24 | )?, 25 | user: time_adjusted( 26 | "user", 27 | next_measurement.stat.user, 28 | self.stat.user, 29 | time_difference, 30 | )?, 31 | nice: time_adjusted( 32 | "nice", 33 | next_measurement.stat.nice, 34 | self.stat.nice, 35 | time_difference, 36 | )?, 37 | system: time_adjusted( 38 | "system", 39 | next_measurement.stat.system, 40 | self.stat.system, 41 | time_difference, 42 | )?, 43 | idle: time_adjusted( 44 | "idle", 45 | next_measurement.stat.idle, 46 | self.stat.idle, 47 | time_difference, 48 | )?, 49 | iowait: time_adjusted( 50 | "iowait", 51 | next_measurement.stat.iowait, 52 | self.stat.iowait, 53 | time_difference, 54 | )?, 55 | irq: time_adjusted( 56 | "irq", 57 | next_measurement.stat.irq, 58 | self.stat.irq, 59 | time_difference, 60 | )?, 61 | softirq: time_adjusted( 62 | "softirq", 63 | next_measurement.stat.softirq, 64 | self.stat.softirq, 65 | time_difference, 66 | )?, 67 | steal: time_adjusted( 68 | "steal", 69 | next_measurement.stat.steal, 70 | self.stat.steal, 71 | time_difference, 72 | )?, 73 | guest: time_adjusted( 74 | "guest", 75 | next_measurement.stat.guest, 76 | self.stat.guest, 77 | time_difference, 78 | )?, 79 | guestnice: time_adjusted( 80 | "guestnice", 81 | next_measurement.stat.guestnice, 82 | self.stat.guestnice, 83 | time_difference, 84 | )?, 85 | }) 86 | } 87 | } 88 | 89 | /// Cpu stats for a minute 90 | #[derive(Debug, PartialEq)] 91 | pub struct CpuStat { 92 | pub total: u64, 93 | pub user: u64, 94 | pub nice: u64, 95 | pub system: u64, 96 | pub idle: u64, 97 | pub iowait: u64, 98 | pub irq: u64, 99 | pub softirq: u64, 100 | pub steal: u64, 101 | pub guest: u64, 102 | pub guestnice: u64, 103 | } 104 | 105 | impl CpuStat { 106 | /// Calculate the weight of the various components in percentages 107 | pub fn in_percentages(&self) -> CpuStatPercentages { 108 | CpuStatPercentages { 109 | total_usage: self.percentage_of_total(self.total - self.idle), 110 | user: self.percentage_of_total(self.user), 111 | nice: self.percentage_of_total(self.nice), 112 | system: self.percentage_of_total(self.system), 113 | idle: self.percentage_of_total(self.idle), 114 | iowait: self.percentage_of_total(self.iowait), 115 | irq: self.percentage_of_total(self.irq), 116 | softirq: self.percentage_of_total(self.softirq), 117 | steal: self.percentage_of_total(self.steal), 118 | guest: self.percentage_of_total(self.guest), 119 | guestnice: self.percentage_of_total(self.guestnice), 120 | } 121 | } 122 | 123 | fn percentage_of_total(&self, value: u64) -> f32 { 124 | (value as f64 / self.total as f64 * 100.0) as f32 125 | } 126 | } 127 | 128 | /// Cpu stats converted to percentages 129 | #[derive(Debug, PartialEq)] 130 | pub struct CpuStatPercentages { 131 | pub total_usage: f32, 132 | pub user: f32, 133 | pub nice: f32, 134 | pub system: f32, 135 | pub idle: f32, 136 | pub iowait: f32, 137 | pub irq: f32, 138 | pub softirq: f32, 139 | pub steal: f32, 140 | pub guest: f32, 141 | pub guestnice: f32, 142 | } 143 | 144 | /// Read the current CPU stats of the system. 145 | #[cfg(target_os = "linux")] 146 | pub fn read() -> Result { 147 | os::read() 148 | } 149 | 150 | #[cfg(target_os = "linux")] 151 | mod os { 152 | use super::super::super::{ 153 | file_to_buf_reader, parse_u64, path_to_string, precise_time_ns, Result, 154 | }; 155 | use super::{CpuMeasurement, CpuStat}; 156 | use crate::error::ProbeError; 157 | use std::io::BufRead; 158 | use std::path::Path; 159 | 160 | #[inline] 161 | pub fn read() -> Result { 162 | read_and_parse_proc_stat(&Path::new("/proc/stat")) 163 | } 164 | 165 | pub fn read_and_parse_proc_stat(path: &Path) -> Result { 166 | let mut line = String::new(); 167 | // columns: user nice system idle iowait irq softirq 168 | let mut reader = file_to_buf_reader(path)?; 169 | let time = precise_time_ns(); 170 | 171 | reader 172 | .read_line(&mut line) 173 | .map_err(|e| ProbeError::IO(e, path_to_string(path)))?; 174 | 175 | let stats: Vec<&str> = line.split_whitespace().skip(1).collect(); 176 | 177 | let length = stats.len(); 178 | if length < 5 { 179 | return Err(ProbeError::UnexpectedContent( 180 | "Incorrect number of stats".to_owned(), 181 | )); 182 | } 183 | 184 | let usertime = parse_u64(stats[0])?; 185 | let nicetime = parse_u64(stats[1])?; 186 | let guest = parse_u64(*stats.get(8).unwrap_or(&"0"))?; 187 | let guestnice = parse_u64(*stats.get(9).unwrap_or(&"0"))?; 188 | 189 | let mut cpu = CpuStat { 190 | total: 0, 191 | user: usertime - guest, 192 | nice: nicetime - guestnice, 193 | system: parse_u64(stats[2])?, 194 | idle: parse_u64(stats[3])?, 195 | iowait: parse_u64(stats[4])?, 196 | irq: parse_u64(*stats.get(5).unwrap_or(&"0"))?, 197 | softirq: parse_u64(*stats.get(6).unwrap_or(&"0"))?, 198 | steal: parse_u64(*stats.get(7).unwrap_or(&"0"))?, 199 | guest, 200 | guestnice, 201 | }; 202 | let idlealltime = cpu.idle + cpu.iowait; 203 | let systemalltime = cpu.system + cpu.irq + cpu.softirq; 204 | let virtualtime = cpu.guest + cpu.guestnice; 205 | cpu.total = cpu.user + cpu.nice + systemalltime + idlealltime + cpu.steal + virtualtime; 206 | 207 | Ok(CpuMeasurement { 208 | precise_time_ns: time, 209 | stat: cpu, 210 | }) 211 | } 212 | } 213 | 214 | #[cfg(test)] 215 | #[cfg(target_os = "linux")] 216 | mod test { 217 | use super::os::read_and_parse_proc_stat; 218 | use super::{CpuMeasurement, CpuStat, CpuStatPercentages}; 219 | use crate::error::ProbeError; 220 | use std::path::Path; 221 | 222 | #[test] 223 | fn test_read_cpu() { 224 | assert!(super::read().is_ok()); 225 | } 226 | 227 | #[test] 228 | fn test_read_proc_measurement() { 229 | let measurement = 230 | read_and_parse_proc_stat(&Path::new("fixtures/linux/cpu/proc_stat")).unwrap(); 231 | let cpu = measurement.stat; 232 | assert_eq!(cpu.total, 39); 233 | assert_eq!(cpu.user, 8); 234 | assert_eq!(cpu.nice, 2); 235 | assert_eq!(cpu.system, 7); 236 | assert_eq!(cpu.idle, 6); 237 | assert_eq!(cpu.iowait, 5); 238 | assert_eq!(cpu.irq, 4); 239 | assert_eq!(cpu.softirq, 3); 240 | assert_eq!(cpu.steal, 1); 241 | assert_eq!(cpu.guest, 2); 242 | assert_eq!(cpu.guestnice, 1); 243 | } 244 | 245 | #[test] 246 | fn test_read_proc_measurement_from_partial() { 247 | let measurement = 248 | read_and_parse_proc_stat(&Path::new("fixtures/linux/cpu/proc_stat_partial")).unwrap(); 249 | let cpu = measurement.stat; 250 | assert_eq!(cpu.total, 31); 251 | assert_eq!(cpu.user, 10); 252 | assert_eq!(cpu.nice, 3); 253 | assert_eq!(cpu.system, 7); 254 | assert_eq!(cpu.idle, 6); 255 | assert_eq!(cpu.iowait, 5); 256 | assert_eq!(cpu.irq, 0); 257 | assert_eq!(cpu.softirq, 0); 258 | assert_eq!(cpu.steal, 0); 259 | assert_eq!(cpu.guest, 0); 260 | assert_eq!(cpu.guestnice, 0); 261 | } 262 | 263 | #[test] 264 | fn test_proc_wrong_path() { 265 | match read_and_parse_proc_stat(&Path::new("bananas")) { 266 | Err(ProbeError::IO(_, _)) => (), 267 | r => panic!("Unexpected result: {:?}", r), 268 | } 269 | } 270 | 271 | #[test] 272 | fn test_read_and_parse_proc_stat_incomplete() { 273 | match read_and_parse_proc_stat(&Path::new("fixtures/linux/cpu/proc_stat_incomplete")) { 274 | Err(ProbeError::UnexpectedContent(_)) => (), 275 | r => panic!("Unexpected result: {:?}", r), 276 | } 277 | } 278 | 279 | #[test] 280 | fn test_read_and_parse_proc_stat_garbage() { 281 | let path = Path::new("fixtures/linux/cpu/proc_stat_garbage"); 282 | match read_and_parse_proc_stat(&path) { 283 | Err(ProbeError::UnexpectedContent(_)) => (), 284 | r => panic!("Unexpected result: {:?}", r), 285 | } 286 | } 287 | 288 | #[test] 289 | fn test_calculate_per_minute_wrong_times() { 290 | let measurement1 = CpuMeasurement { 291 | precise_time_ns: 90_000_000_000, 292 | stat: CpuStat { 293 | total: 0, 294 | user: 0, 295 | nice: 0, 296 | system: 0, 297 | idle: 0, 298 | iowait: 0, 299 | irq: 0, 300 | softirq: 0, 301 | steal: 0, 302 | guest: 0, 303 | guestnice: 0, 304 | }, 305 | }; 306 | 307 | let measurement2 = CpuMeasurement { 308 | precise_time_ns: 60_000_000_000, 309 | stat: CpuStat { 310 | total: 0, 311 | user: 0, 312 | nice: 0, 313 | system: 0, 314 | idle: 0, 315 | iowait: 0, 316 | irq: 0, 317 | softirq: 0, 318 | steal: 0, 319 | guest: 0, 320 | guestnice: 0, 321 | }, 322 | }; 323 | 324 | match measurement1.calculate_per_minute(&measurement2) { 325 | Err(ProbeError::InvalidInput(_)) => (), 326 | r => panic!("Unexpected result: {:?}", r), 327 | } 328 | } 329 | 330 | #[test] 331 | fn test_calculate_per_minute_full_minute() { 332 | let measurement1 = CpuMeasurement { 333 | precise_time_ns: 60_000_000_000, 334 | stat: CpuStat { 335 | total: 6380, 336 | user: 1000, 337 | nice: 1100, 338 | system: 1200, 339 | idle: 1300, 340 | iowait: 1400, 341 | irq: 50, 342 | softirq: 10, 343 | steal: 20, 344 | guest: 200, 345 | guestnice: 100, 346 | }, 347 | }; 348 | 349 | let measurement2 = CpuMeasurement { 350 | precise_time_ns: 120_000_000_000, 351 | stat: CpuStat { 352 | total: 6440, 353 | user: 1006, 354 | nice: 1106, 355 | system: 1206, 356 | idle: 1306, 357 | iowait: 1406, 358 | irq: 56, 359 | softirq: 16, 360 | steal: 26, 361 | guest: 206, 362 | guestnice: 106, 363 | }, 364 | }; 365 | 366 | let expected = CpuStat { 367 | total: 60, 368 | user: 6, 369 | nice: 6, 370 | system: 6, 371 | idle: 6, 372 | iowait: 6, 373 | irq: 6, 374 | softirq: 6, 375 | steal: 6, 376 | guest: 6, 377 | guestnice: 6, 378 | }; 379 | 380 | let stat = measurement1.calculate_per_minute(&measurement2).unwrap(); 381 | 382 | assert_eq!(stat, expected); 383 | } 384 | 385 | #[test] 386 | fn test_calculate_per_minute_partial_minute() { 387 | let measurement1 = CpuMeasurement { 388 | precise_time_ns: 60_000_000_000, 389 | stat: CpuStat { 390 | total: 6380, 391 | user: 1000, 392 | nice: 1100, 393 | system: 1200, 394 | idle: 1300, 395 | iowait: 1400, 396 | irq: 50, 397 | softirq: 10, 398 | steal: 20, 399 | guest: 200, 400 | guestnice: 100, 401 | }, 402 | }; 403 | 404 | let measurement2 = CpuMeasurement { 405 | precise_time_ns: 90_000_000_000, 406 | stat: CpuStat { 407 | total: 6440, 408 | user: 1006, 409 | nice: 1106, 410 | system: 1206, 411 | idle: 1306, 412 | iowait: 1406, 413 | irq: 56, 414 | softirq: 16, 415 | steal: 26, 416 | guest: 206, 417 | guestnice: 106, 418 | }, 419 | }; 420 | 421 | let expected = CpuStat { 422 | total: 120, 423 | user: 12, 424 | nice: 12, 425 | system: 12, 426 | idle: 12, 427 | iowait: 12, 428 | irq: 12, 429 | softirq: 12, 430 | steal: 12, 431 | guest: 12, 432 | guestnice: 12, 433 | }; 434 | 435 | let stat = measurement1.calculate_per_minute(&measurement2).unwrap(); 436 | 437 | assert_eq!(stat, expected); 438 | } 439 | 440 | #[test] 441 | fn test_calculate_per_minute_values_lower() { 442 | let measurement1 = CpuMeasurement { 443 | precise_time_ns: 60_000_000_000, 444 | stat: CpuStat { 445 | total: 6380, 446 | user: 1000, 447 | nice: 1100, 448 | system: 1200, 449 | idle: 1300, 450 | iowait: 1400, 451 | irq: 50, 452 | softirq: 10, 453 | steal: 20, 454 | guest: 200, 455 | guestnice: 100, 456 | }, 457 | }; 458 | 459 | let measurement2 = CpuMeasurement { 460 | precise_time_ns: 90_000_000_000, 461 | stat: CpuStat { 462 | total: 1040, 463 | user: 106, 464 | nice: 116, 465 | system: 126, 466 | idle: 136, 467 | iowait: 146, 468 | irq: 56, 469 | softirq: 16, 470 | steal: 26, 471 | guest: 206, 472 | guestnice: 106, 473 | }, 474 | }; 475 | 476 | match measurement1.calculate_per_minute(&measurement2) { 477 | Err(ProbeError::UnexpectedContent(_)) => (), 478 | r => panic!("Unexpected result: {:?}", r), 479 | } 480 | } 481 | 482 | #[test] 483 | fn test_in_percentages() { 484 | let stat = CpuStat { 485 | total: 1000, 486 | user: 450, 487 | nice: 70, 488 | system: 100, 489 | idle: 100, 490 | iowait: 120, 491 | irq: 10, 492 | softirq: 20, 493 | steal: 50, 494 | guest: 50, 495 | guestnice: 30, 496 | }; 497 | 498 | let expected = CpuStatPercentages { 499 | total_usage: 90.0, 500 | user: 45.0, 501 | nice: 7.0, 502 | system: 10.0, 503 | idle: 10.0, 504 | iowait: 12.0, 505 | irq: 1.0, 506 | softirq: 2.0, 507 | steal: 5.0, 508 | guest: 5.0, 509 | guestnice: 3.0, 510 | }; 511 | 512 | assert_eq!(stat.in_percentages(), expected); 513 | } 514 | 515 | #[test] 516 | fn test_in_percentages_fractions() { 517 | let stat = CpuStat { 518 | total: 1000, 519 | user: 445, 520 | nice: 65, 521 | system: 100, 522 | idle: 100, 523 | iowait: 147, 524 | irq: 1, 525 | softirq: 2, 526 | steal: 50, 527 | guest: 55, 528 | guestnice: 35, 529 | }; 530 | 531 | let expected = CpuStatPercentages { 532 | total_usage: 90.0, 533 | user: 44.5, 534 | nice: 6.5, 535 | system: 10.0, 536 | idle: 10.0, 537 | iowait: 14.7, 538 | irq: 0.1, 539 | softirq: 0.2, 540 | steal: 5.0, 541 | guest: 5.5, 542 | guestnice: 3.5, 543 | }; 544 | 545 | assert_eq!(stat.in_percentages(), expected); 546 | } 547 | 548 | #[test] 549 | fn test_in_percentages_integration() { 550 | let mut measurement1 = 551 | read_and_parse_proc_stat(&Path::new("fixtures/linux/cpu/proc_stat_1")).unwrap(); 552 | measurement1.precise_time_ns = 60_000_000_000; 553 | let mut measurement2 = 554 | read_and_parse_proc_stat(&Path::new("fixtures/linux/cpu/proc_stat_2")).unwrap(); 555 | measurement2.precise_time_ns = 120_000_000_000; 556 | 557 | let stat = measurement1.calculate_per_minute(&measurement2).unwrap(); 558 | let in_percentages = stat.in_percentages(); 559 | 560 | // Rounding in the floating point calculations can vary, so check if this 561 | // is in the correct range. 562 | 563 | assert!(in_percentages.user > 4.0); 564 | assert!(in_percentages.user < 5.0); 565 | 566 | assert!(in_percentages.nice < 0.21); 567 | assert!(in_percentages.nice > 0.2); 568 | 569 | assert!(in_percentages.system > 1.51); 570 | assert!(in_percentages.system < 1.52); 571 | 572 | assert!(in_percentages.idle > 92.3); 573 | assert!(in_percentages.idle < 92.4); 574 | 575 | assert!(in_percentages.iowait < 0.05); 576 | assert!(in_percentages.iowait > 0.04); 577 | 578 | assert!(in_percentages.irq < 0.5); 579 | assert!(in_percentages.irq > 0.49); 580 | 581 | assert!(in_percentages.softirq > 0.028); 582 | assert!(in_percentages.softirq < 0.029); 583 | 584 | assert!(in_percentages.steal < 0.41); 585 | assert!(in_percentages.steal > 0.40); 586 | 587 | assert!(in_percentages.guest < 0.41); 588 | assert!(in_percentages.guest > 0.40); 589 | 590 | assert!(in_percentages.guestnice < 0.21); 591 | assert!(in_percentages.guestnice > 0.20); 592 | 593 | // The total of these values should be 100. 594 | let idlealltime = in_percentages.idle + in_percentages.iowait; 595 | let systemalltime = in_percentages.system + in_percentages.irq + in_percentages.softirq; 596 | let virtualtime = in_percentages.guest + in_percentages.guestnice; 597 | let total = (in_percentages.user 598 | + in_percentages.nice 599 | + systemalltime 600 | + idlealltime 601 | + in_percentages.steal 602 | + virtualtime) as f64; 603 | 604 | assert!(total < 100.1); 605 | assert!(total > 99.9); 606 | } 607 | } 608 | -------------------------------------------------------------------------------- /src/disk_stats.rs: -------------------------------------------------------------------------------- 1 | use super::{calculate_time_difference, time_adjusted, Result}; 2 | use crate::error::ProbeError; 3 | use std::collections::HashMap; 4 | use std::path::Path; 5 | 6 | pub type DiskStats = HashMap; 7 | 8 | #[derive(Debug, PartialEq)] 9 | pub struct DiskStatsMeasurement { 10 | pub precise_time_ns: u64, 11 | pub stats: DiskStats, 12 | } 13 | 14 | impl DiskStatsMeasurement { 15 | /// Calculate the disk stats per minute based on this measurement and a measurement in the 16 | /// future. It is advisable to make the next measurement roughly a minute from this one for the 17 | /// most reliable result. 18 | pub fn calculate_per_minute( 19 | &self, 20 | next_measurement: &DiskStatsMeasurement, 21 | ) -> Result { 22 | let time_difference = 23 | calculate_time_difference(self.precise_time_ns, next_measurement.precise_time_ns)?; 24 | 25 | let mut stats = HashMap::new(); 26 | 27 | for (name, stat) in self.stats.iter() { 28 | let next_stat = match next_measurement.stats.get(name) { 29 | Some(stat) => stat, 30 | None => { 31 | return Err(ProbeError::UnexpectedContent(format!( 32 | "{} is not present in the next measurement", 33 | name 34 | ))) 35 | } 36 | }; 37 | 38 | stats.insert( 39 | name.to_owned(), 40 | DiskStat { 41 | reads_completed_successfully: time_adjusted( 42 | "reads_completed_successfully", 43 | next_stat.reads_completed_successfully, 44 | stat.reads_completed_successfully, 45 | time_difference, 46 | )?, 47 | reads_merged: time_adjusted( 48 | "reads_merged", 49 | next_stat.reads_merged, 50 | stat.reads_merged, 51 | time_difference, 52 | )?, 53 | sectors_read: time_adjusted( 54 | "sectors_read", 55 | next_stat.sectors_read, 56 | stat.sectors_read, 57 | time_difference, 58 | )?, 59 | time_spent_reading_ms: time_adjusted( 60 | "time_spent_reading_ms", 61 | next_stat.time_spent_reading_ms, 62 | stat.time_spent_reading_ms, 63 | time_difference, 64 | )?, 65 | writes_completed: time_adjusted( 66 | "writes_completed", 67 | next_stat.writes_completed, 68 | stat.writes_completed, 69 | time_difference, 70 | )?, 71 | writes_merged: time_adjusted( 72 | "writes_merged", 73 | next_stat.writes_merged, 74 | stat.writes_merged, 75 | time_difference, 76 | )?, 77 | sectors_written: time_adjusted( 78 | "sectors_written", 79 | next_stat.sectors_written, 80 | stat.sectors_written, 81 | time_difference, 82 | )?, 83 | time_spent_writing_ms: time_adjusted( 84 | "time_spent_writing_ms", 85 | next_stat.time_spent_writing_ms, 86 | stat.time_spent_writing_ms, 87 | time_difference, 88 | )?, 89 | ios_currently_in_progress: time_adjusted( 90 | "ios_currently_in_progress", 91 | next_stat.ios_currently_in_progress, 92 | stat.ios_currently_in_progress, 93 | time_difference, 94 | )?, 95 | time_spent_doing_ios_ms: time_adjusted( 96 | "time_spent_doing_ios_ms", 97 | next_stat.time_spent_doing_ios_ms, 98 | stat.time_spent_doing_ios_ms, 99 | time_difference, 100 | )?, 101 | weighted_time_spent_doing_ios_ms: time_adjusted( 102 | "weighted_time_spent_doing_ios_ms", 103 | next_stat.weighted_time_spent_doing_ios_ms, 104 | stat.weighted_time_spent_doing_ios_ms, 105 | time_difference, 106 | )?, 107 | }, 108 | ); 109 | } 110 | 111 | Ok(DiskStatsPerMinute { stats }) 112 | } 113 | } 114 | 115 | #[derive(Debug, PartialEq)] 116 | pub struct DiskStat { 117 | pub reads_completed_successfully: u64, 118 | pub reads_merged: u64, 119 | pub sectors_read: u64, 120 | pub time_spent_reading_ms: u64, 121 | pub writes_completed: u64, 122 | pub writes_merged: u64, 123 | pub sectors_written: u64, 124 | pub time_spent_writing_ms: u64, 125 | pub ios_currently_in_progress: u64, 126 | pub time_spent_doing_ios_ms: u64, 127 | pub weighted_time_spent_doing_ios_ms: u64, 128 | } 129 | 130 | impl DiskStat { 131 | pub fn bytes_read(&self) -> u64 { 132 | self.sectors_read * 512 133 | } 134 | 135 | pub fn bytes_written(&self) -> u64 { 136 | self.sectors_written * 512 137 | } 138 | } 139 | 140 | #[derive(Debug, PartialEq)] 141 | pub struct DiskStatsPerMinute { 142 | pub stats: DiskStats, 143 | } 144 | 145 | #[cfg(target_os = "linux")] 146 | pub fn read() -> Result { 147 | os::read_and_parse_proc_diskstats(&Path::new("/proc/diskstats")) 148 | } 149 | 150 | #[cfg(target_os = "linux")] 151 | mod os { 152 | use super::super::{ 153 | file_to_buf_reader, parse_u64, path_to_string, precise_time_ns, ProbeError, Result, 154 | }; 155 | use super::{DiskStat, DiskStatsMeasurement}; 156 | use std::collections::HashMap; 157 | use std::io::BufRead; 158 | use std::path::Path; 159 | 160 | #[inline] 161 | pub fn read_and_parse_proc_diskstats(path: &Path) -> Result { 162 | let reader = file_to_buf_reader(path)?; 163 | 164 | let mut out = DiskStatsMeasurement { 165 | precise_time_ns: precise_time_ns(), 166 | stats: HashMap::new(), 167 | }; 168 | 169 | for line_result in reader.lines() { 170 | let line = line_result.map_err(|e| ProbeError::IO(e, path_to_string(path)))?; 171 | let segments: Vec<&str> = line.split_whitespace().collect(); 172 | 173 | // /proc/diskstats has 14 fields, or 18 fields for kernel 4.18+, or 20 in kernel 5.5+ 174 | // https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats 175 | if segments.len() != 14 && segments.len() != 18 && segments.len() != 20 { 176 | return Err(ProbeError::UnexpectedContent( 177 | "Incorrect number of segments".to_owned(), 178 | )); 179 | } 180 | 181 | let disk_stat = DiskStat { 182 | reads_completed_successfully: parse_u64(segments[3])?, 183 | reads_merged: parse_u64(segments[4])?, 184 | sectors_read: parse_u64(segments[5])?, 185 | time_spent_reading_ms: parse_u64(segments[6])?, 186 | writes_completed: parse_u64(segments[7])?, 187 | writes_merged: parse_u64(segments[8])?, 188 | sectors_written: parse_u64(segments[9])?, 189 | time_spent_writing_ms: parse_u64(segments[10])?, 190 | ios_currently_in_progress: parse_u64(segments[11])?, 191 | time_spent_doing_ios_ms: parse_u64(segments[12])?, 192 | weighted_time_spent_doing_ios_ms: parse_u64(segments[13])?, 193 | }; 194 | 195 | out.stats.insert(segments[2].to_owned(), disk_stat); 196 | } 197 | 198 | Ok(out) 199 | } 200 | } 201 | 202 | #[cfg(test)] 203 | #[cfg(target_os = "linux")] 204 | mod tests { 205 | use super::os::read_and_parse_proc_diskstats; 206 | use super::DiskStatsMeasurement; 207 | use crate::error::ProbeError; 208 | use std::collections::HashMap; 209 | use std::path::Path; 210 | 211 | #[test] 212 | fn test_read_disk_stats() { 213 | assert!(super::read().is_ok()); 214 | } 215 | 216 | #[test] 217 | fn test_read_and_parse_proc_diskstats() { 218 | let measurement = 219 | read_and_parse_proc_diskstats(&Path::new("fixtures/linux/disk_stats/proc_diskstats")) 220 | .unwrap(); 221 | 222 | assert!(measurement.precise_time_ns > 0); 223 | 224 | assert_eq!(2, measurement.stats.len()); 225 | 226 | let sda = measurement.stats.get("sda").unwrap(); 227 | assert_eq!(6185, sda.reads_completed_successfully); 228 | assert_eq!(9367, sda.reads_merged); 229 | assert_eq!(403272, sda.sectors_read); 230 | assert_eq!(206475264, sda.bytes_read()); 231 | assert_eq!(22160, sda.time_spent_reading_ms); 232 | assert_eq!(2591, sda.writes_completed); 233 | assert_eq!(8251, sda.writes_merged); 234 | assert_eq!(84452, sda.sectors_written); 235 | assert_eq!(43239424, sda.bytes_written()); 236 | assert_eq!(2860, sda.time_spent_writing_ms); 237 | assert_eq!(0, sda.ios_currently_in_progress); 238 | assert_eq!(8960, sda.time_spent_doing_ios_ms); 239 | assert_eq!(24990, sda.weighted_time_spent_doing_ios_ms); 240 | 241 | let sda1 = measurement.stats.get("sda1").unwrap(); 242 | assert_eq!(483, sda1.reads_completed_successfully); 243 | assert_eq!(4782, sda1.reads_merged); 244 | assert_eq!(41466, sda1.sectors_read); 245 | assert_eq!(21230592, sda1.bytes_read()); 246 | assert_eq!(1100, sda1.time_spent_reading_ms); 247 | assert_eq!(7, sda1.writes_completed); 248 | assert_eq!(1, sda1.writes_merged); 249 | assert_eq!(28, sda1.sectors_written); 250 | assert_eq!(14336, sda1.bytes_written()); 251 | assert_eq!(40, sda1.time_spent_writing_ms); 252 | assert_eq!(0, sda1.ios_currently_in_progress); 253 | assert_eq!(930, sda1.time_spent_doing_ios_ms); 254 | assert_eq!(1140, sda1.weighted_time_spent_doing_ios_ms); 255 | } 256 | 257 | #[test] 258 | fn test_read_and_parse_proc_diskstats_kernel_4_18_plus() { 259 | let measurement = read_and_parse_proc_diskstats(&Path::new( 260 | "fixtures/linux/disk_stats/proc_diskstats_4_18", 261 | )) 262 | .unwrap(); 263 | 264 | assert!(measurement.precise_time_ns > 0); 265 | 266 | assert_eq!(2, measurement.stats.len()); 267 | 268 | let sda = measurement.stats.get("sda").unwrap(); 269 | assert_eq!(6185, sda.reads_completed_successfully); 270 | assert_eq!(9367, sda.reads_merged); 271 | assert_eq!(403272, sda.sectors_read); 272 | assert_eq!(206475264, sda.bytes_read()); 273 | assert_eq!(22160, sda.time_spent_reading_ms); 274 | assert_eq!(2591, sda.writes_completed); 275 | assert_eq!(8251, sda.writes_merged); 276 | assert_eq!(84452, sda.sectors_written); 277 | assert_eq!(43239424, sda.bytes_written()); 278 | assert_eq!(2860, sda.time_spent_writing_ms); 279 | assert_eq!(0, sda.ios_currently_in_progress); 280 | assert_eq!(8960, sda.time_spent_doing_ios_ms); 281 | assert_eq!(24990, sda.weighted_time_spent_doing_ios_ms); 282 | 283 | let sda1 = measurement.stats.get("sda1").unwrap(); 284 | assert_eq!(483, sda1.reads_completed_successfully); 285 | assert_eq!(4782, sda1.reads_merged); 286 | assert_eq!(41466, sda1.sectors_read); 287 | assert_eq!(21230592, sda1.bytes_read()); 288 | assert_eq!(1100, sda1.time_spent_reading_ms); 289 | assert_eq!(7, sda1.writes_completed); 290 | assert_eq!(1, sda1.writes_merged); 291 | assert_eq!(28, sda1.sectors_written); 292 | assert_eq!(14336, sda1.bytes_written()); 293 | assert_eq!(40, sda1.time_spent_writing_ms); 294 | assert_eq!(0, sda1.ios_currently_in_progress); 295 | assert_eq!(930, sda1.time_spent_doing_ios_ms); 296 | assert_eq!(1140, sda1.weighted_time_spent_doing_ios_ms); 297 | } 298 | 299 | #[test] 300 | fn test_read_and_parse_proc_diskstats_kernel_5_5_plus() { 301 | let measurement = read_and_parse_proc_diskstats(&Path::new( 302 | "fixtures/linux/disk_stats/proc_diskstats_5_5", 303 | )) 304 | .unwrap(); 305 | 306 | assert!(measurement.precise_time_ns > 0); 307 | 308 | assert_eq!(7, measurement.stats.len()); 309 | 310 | let xvda = measurement.stats.get("xvda").unwrap(); 311 | 312 | assert_eq!(11514, xvda.reads_completed_successfully); 313 | assert_eq!(271, xvda.reads_merged); 314 | assert_eq!(1204763, xvda.sectors_read); 315 | assert_eq!(616838656, xvda.bytes_read()); 316 | assert_eq!(14486, xvda.time_spent_reading_ms); 317 | assert_eq!(130975, xvda.writes_completed); 318 | assert_eq!(18219, xvda.writes_merged); 319 | assert_eq!(2798097, xvda.sectors_written); 320 | assert_eq!(1432625664, xvda.bytes_written()); 321 | assert_eq!(178789, xvda.time_spent_writing_ms); 322 | assert_eq!(0, xvda.ios_currently_in_progress); 323 | assert_eq!(42381, xvda.time_spent_doing_ios_ms); 324 | assert_eq!(112571, xvda.weighted_time_spent_doing_ios_ms); 325 | 326 | let xvdf = measurement.stats.get("xvdf").unwrap(); 327 | 328 | assert_eq!(25663, xvdf.reads_completed_successfully); 329 | assert_eq!(55, xvdf.reads_merged); 330 | assert_eq!(810823, xvdf.sectors_read); 331 | assert_eq!(415141376, xvdf.bytes_read()); 332 | assert_eq!(29159, xvdf.time_spent_reading_ms); 333 | assert_eq!(33077, xvdf.writes_completed); 334 | assert_eq!(21044, xvdf.writes_merged); 335 | assert_eq!(1430229, xvdf.sectors_written); 336 | assert_eq!(732277248, xvdf.bytes_written()); 337 | assert_eq!(37739, xvdf.time_spent_writing_ms); 338 | assert_eq!(0, xvdf.ios_currently_in_progress); 339 | assert_eq!(42594, xvdf.time_spent_doing_ios_ms); 340 | assert_eq!(33450, xvdf.weighted_time_spent_doing_ios_ms); 341 | } 342 | 343 | #[test] 344 | fn test_read_and_parse_proc_diskstats_incomplete() { 345 | match read_and_parse_proc_diskstats(&Path::new( 346 | "fixtures/linux/disk_stats/proc_diskstats_incomplete", 347 | )) { 348 | Err(ProbeError::UnexpectedContent(_)) => (), 349 | r => panic!("Unexpected result: {:?}", r), 350 | } 351 | } 352 | 353 | #[test] 354 | fn test_read_and_parse_proc_diskstats_garbage() { 355 | match read_and_parse_proc_diskstats(&Path::new( 356 | "fixtures/linux/disk_stats/proc_diskstats_garbage", 357 | )) { 358 | Err(ProbeError::UnexpectedContent(_)) => (), 359 | r => panic!("Unexpected result: {:?}", r), 360 | } 361 | } 362 | 363 | #[test] 364 | fn test_calculate_per_minute_full_minute() { 365 | let mut stats1 = HashMap::new(); 366 | stats1.insert("sda1".to_owned(), helpers::disk_stat(0)); 367 | let measurement1 = DiskStatsMeasurement { 368 | precise_time_ns: 60_000_000_000, 369 | stats: stats1, 370 | }; 371 | let mut stats2 = HashMap::new(); 372 | stats2.insert("sda1".to_owned(), helpers::disk_stat(120)); 373 | let measurement2 = DiskStatsMeasurement { 374 | precise_time_ns: 120_000_000_000, 375 | stats: stats2, 376 | }; 377 | 378 | let per_minute = measurement1.calculate_per_minute(&measurement2).unwrap(); 379 | let sda1 = per_minute.stats.get("sda1").unwrap(); 380 | assert_eq!(sda1.reads_completed_successfully, 120); 381 | assert_eq!(sda1.reads_merged, 120); 382 | assert_eq!(sda1.sectors_read, 120); 383 | assert_eq!(sda1.time_spent_reading_ms, 120); 384 | assert_eq!(sda1.writes_completed, 120); 385 | assert_eq!(sda1.writes_merged, 120); 386 | assert_eq!(sda1.sectors_written, 120); 387 | assert_eq!(sda1.time_spent_writing_ms, 120); 388 | assert_eq!(sda1.ios_currently_in_progress, 120); 389 | assert_eq!(sda1.time_spent_doing_ios_ms, 120); 390 | assert_eq!(sda1.weighted_time_spent_doing_ios_ms, 120); 391 | } 392 | 393 | #[test] 394 | fn test_calculate_per_minute_partial_minute() { 395 | let mut stats1 = HashMap::new(); 396 | stats1.insert("sda1".to_owned(), helpers::disk_stat(0)); 397 | let measurement1 = DiskStatsMeasurement { 398 | precise_time_ns: 60_000_000_000, 399 | stats: stats1, 400 | }; 401 | let mut stats2 = HashMap::new(); 402 | stats2.insert("sda1".to_owned(), helpers::disk_stat(120)); 403 | let measurement2 = DiskStatsMeasurement { 404 | precise_time_ns: 90_000_000_000, 405 | stats: stats2, 406 | }; 407 | 408 | let per_minute = measurement1.calculate_per_minute(&measurement2).unwrap(); 409 | let sda1 = per_minute.stats.get("sda1").unwrap(); 410 | assert_eq!(sda1.reads_completed_successfully, 240); 411 | assert_eq!(sda1.reads_merged, 240); 412 | assert_eq!(sda1.sectors_read, 240); 413 | assert_eq!(sda1.time_spent_reading_ms, 240); 414 | assert_eq!(sda1.writes_completed, 240); 415 | assert_eq!(sda1.writes_merged, 240); 416 | assert_eq!(sda1.sectors_written, 240); 417 | assert_eq!(sda1.time_spent_writing_ms, 240); 418 | assert_eq!(sda1.ios_currently_in_progress, 240); 419 | assert_eq!(sda1.time_spent_doing_ios_ms, 240); 420 | assert_eq!(sda1.weighted_time_spent_doing_ios_ms, 240); 421 | } 422 | 423 | #[test] 424 | fn test_calculate_per_minute_wrong_times() { 425 | let measurement1 = DiskStatsMeasurement { 426 | precise_time_ns: 500, 427 | stats: HashMap::new(), 428 | }; 429 | let measurement2 = DiskStatsMeasurement { 430 | precise_time_ns: 300, 431 | stats: HashMap::new(), 432 | }; 433 | 434 | match measurement1.calculate_per_minute(&measurement2) { 435 | Err(ProbeError::InvalidInput(_)) => (), 436 | r => panic!("Unexpected result: {:?}", r), 437 | } 438 | } 439 | 440 | #[test] 441 | fn test_calculate_per_minute_values_lower() { 442 | let mut stats1 = HashMap::new(); 443 | stats1.insert("sda1".to_owned(), helpers::disk_stat(500)); 444 | let measurement1 = DiskStatsMeasurement { 445 | precise_time_ns: 500, 446 | stats: stats1, 447 | }; 448 | let mut stats2 = HashMap::new(); 449 | stats2.insert("sda1".to_owned(), helpers::disk_stat(400)); 450 | let measurement2 = DiskStatsMeasurement { 451 | precise_time_ns: 600, 452 | stats: stats2, 453 | }; 454 | 455 | match measurement1.calculate_per_minute(&measurement2) { 456 | Err(ProbeError::UnexpectedContent(_)) => (), 457 | r => panic!("Unexpected result: {:?}", r), 458 | } 459 | } 460 | 461 | #[test] 462 | fn test_calculate_per_minute_different_disks() { 463 | let mut stats1 = HashMap::new(); 464 | stats1.insert("sda1".to_owned(), helpers::disk_stat(500)); 465 | let measurement1 = DiskStatsMeasurement { 466 | precise_time_ns: 500, 467 | stats: stats1, 468 | }; 469 | let mut stats2 = HashMap::new(); 470 | stats2.insert("sda2".to_owned(), helpers::disk_stat(600)); 471 | let measurement2 = DiskStatsMeasurement { 472 | precise_time_ns: 600, 473 | stats: stats2, 474 | }; 475 | 476 | match measurement1.calculate_per_minute(&measurement2) { 477 | Err(ProbeError::UnexpectedContent(_)) => (), 478 | r => panic!("Unexpected result: {:?}", r), 479 | } 480 | } 481 | 482 | mod helpers { 483 | use super::super::DiskStat; 484 | 485 | pub fn disk_stat(value: u64) -> DiskStat { 486 | DiskStat { 487 | reads_completed_successfully: value, 488 | reads_merged: value, 489 | sectors_read: value, 490 | time_spent_reading_ms: value, 491 | writes_completed: value, 492 | writes_merged: value, 493 | sectors_written: value, 494 | time_spent_writing_ms: value, 495 | ios_currently_in_progress: value, 496 | time_spent_doing_ios_ms: value, 497 | weighted_time_spent_doing_ios_ms: value, 498 | } 499 | } 500 | } 501 | } 502 | -------------------------------------------------------------------------------- /src/disk_usage.rs: -------------------------------------------------------------------------------- 1 | use super::Result; 2 | 3 | #[derive(Debug, PartialEq)] 4 | pub struct DiskUsage { 5 | pub filesystem: Option, 6 | pub one_k_blocks: u64, 7 | pub one_k_blocks_used: u64, 8 | pub one_k_blocks_free: u64, 9 | pub used_percentage: u32, 10 | pub mountpoint: String, 11 | } 12 | 13 | #[derive(Debug, PartialEq)] 14 | pub struct DiskInodeUsage { 15 | pub filesystem: Option, 16 | pub inodes: u64, 17 | pub iused: u64, 18 | pub ifree: u64, 19 | pub iused_percentage: u32, 20 | pub mountpoint: String, 21 | } 22 | 23 | /// Read the current usage of all disks 24 | #[cfg(target_os = "linux")] 25 | pub fn read() -> Result> { 26 | os::read() 27 | } 28 | 29 | /// Read the current inode usage of all disks 30 | #[cfg(target_os = "linux")] 31 | pub fn read_inodes() -> Result> { 32 | os::read_inodes() 33 | } 34 | 35 | #[cfg(target_os = "linux")] 36 | mod os { 37 | use super::super::{parse_u64, ProbeError, Result}; 38 | use super::{DiskInodeUsage, DiskUsage}; 39 | use std::process::Command; 40 | 41 | #[inline] 42 | pub fn read() -> Result> { 43 | let mut out: Vec = Vec::new(); 44 | let local_out = match disk_fs_local_raw(Some(&["--local"])) { 45 | Ok(o) => o, 46 | Err(_) => match disk_fs_local_raw(None) { 47 | Ok(o) => o, 48 | Err(e) => return Err(e), 49 | }, 50 | }; 51 | 52 | let parsed = parse_df_output(&local_out)?; 53 | 54 | for segment in parsed.iter() { 55 | let usage = DiskUsage { 56 | filesystem: parse_filesystem(segment[0]), 57 | one_k_blocks: parse_u64(segment[1])?, 58 | one_k_blocks_used: parse_u64(segment[2])?, 59 | one_k_blocks_free: parse_u64(segment[3])?, 60 | used_percentage: parse_percentage_segment(segment[4])?, 61 | mountpoint: segment[5].to_string(), 62 | }; 63 | 64 | out.push(usage); 65 | } 66 | 67 | Ok(out) 68 | } 69 | 70 | #[inline] 71 | pub fn read_inodes() -> Result> { 72 | let inodes_out = disk_fs_inodes_raw()?; 73 | parse_df_inodes_output(parse_df_output(&inodes_out)?) 74 | } 75 | 76 | #[inline] 77 | pub fn parse_df_inodes_output(parsed_segments: Vec>) -> Result> { 78 | let mut out: Vec = Vec::new(); 79 | 80 | for segment in parsed_segments.iter() { 81 | let iuse_percentage = segment[4]; 82 | if iuse_percentage == "-" { 83 | continue; 84 | } 85 | let usage = DiskInodeUsage { 86 | filesystem: parse_filesystem(segment[0]), 87 | inodes: parse_u64(segment[1])?, 88 | iused: parse_u64(segment[2])?, 89 | ifree: parse_u64(segment[3])?, 90 | iused_percentage: parse_percentage_segment(iuse_percentage)?, 91 | mountpoint: segment[5].to_string(), 92 | }; 93 | 94 | out.push(usage); 95 | } 96 | 97 | Ok(out) 98 | } 99 | 100 | #[inline] 101 | pub fn parse_df_output(output: &str) -> Result>> { 102 | let mut out: Vec> = Vec::new(); 103 | 104 | // Sometimes the filesystem is on a separate line 105 | let mut filesystem_on_previous_line: Option<&str> = None; 106 | 107 | for line in output.split("\n").skip(1) { 108 | let mut segments: Vec<&str> = line.split_whitespace().collect(); 109 | 110 | match segments.len() { 111 | 0 => { 112 | // Skip 113 | } 114 | 1 => filesystem_on_previous_line = Some(segments[0]), 115 | 5 => { 116 | // Filesystem should be on the previous line 117 | if let Some(fs) = filesystem_on_previous_line { 118 | // Get filesystem first 119 | let mut disk = vec![fs]; 120 | disk.append(&mut segments); 121 | 122 | out.push(disk); 123 | 124 | // Reset this to none 125 | filesystem_on_previous_line = None; 126 | } else { 127 | return Err(ProbeError::UnexpectedContent( 128 | "filesystem expected on previous line".to_owned(), 129 | )); 130 | } 131 | } 132 | 6 => { 133 | // All information is on 1 line 134 | out.push(segments); 135 | } 136 | _ => { 137 | return Err(ProbeError::UnexpectedContent( 138 | "Incorrect number of segments".to_owned(), 139 | )); 140 | } 141 | } 142 | } 143 | 144 | Ok(out) 145 | } 146 | 147 | #[inline] 148 | fn parse_percentage_segment(segment: &str) -> Result { 149 | // Strip % from the used value 150 | let segment_minus_percentage = &segment[..segment.len() - 1]; 151 | 152 | segment_minus_percentage.parse().map_err(|_| { 153 | ProbeError::UnexpectedContent("Could not parse percentage segment".to_owned()) 154 | }) 155 | } 156 | 157 | #[inline] 158 | fn parse_filesystem(segment: &str) -> Option { 159 | match segment { 160 | "none" => None, 161 | value => Some(value.to_string()), 162 | } 163 | } 164 | 165 | #[inline] 166 | fn disk_fs_inodes_raw() -> Result { 167 | let output = Command::new("df") 168 | .arg("-i") 169 | .output() 170 | .map_err(|e| ProbeError::IO(e, "df -i".to_owned()))? 171 | .stdout; 172 | 173 | Ok(String::from_utf8_lossy(&output).to_string()) 174 | } 175 | 176 | #[inline] 177 | fn disk_fs_local_raw(options: Option<&[&str]>) -> Result { 178 | let mut cmd = Command::new("df"); 179 | if let Some(opts) = options { 180 | cmd.args(opts); 181 | } 182 | 183 | let output = cmd 184 | .output() 185 | .map_err(|e| ProbeError::IO(e, format!("df {}", options.unwrap_or(&[]).join(" "))))?; 186 | let stdout = output.stdout; 187 | let status = output.status; 188 | 189 | if status.success() { 190 | Ok(String::from_utf8_lossy(&stdout).to_string()) 191 | } else { 192 | Err(ProbeError::StatusFailure(format!( 193 | "Command `df` returned failure exit code '{:?}': df {}", 194 | status.code(), 195 | options.unwrap_or(&[]).join(" ") 196 | ))) 197 | } 198 | } 199 | } 200 | 201 | #[cfg(test)] 202 | #[cfg(target_os = "linux")] 203 | mod tests { 204 | use super::super::file_to_string; 205 | use super::super::ProbeError; 206 | use std::path::Path; 207 | 208 | #[test] 209 | fn test_read_disks() { 210 | assert!(super::read().is_ok()); 211 | assert!(!super::read().unwrap().is_empty()); 212 | } 213 | 214 | #[test] 215 | fn test_parse_df_output() { 216 | let expected = vec![ 217 | vec![ 218 | "/dev/mapper/lucid64-root", 219 | "81234688", 220 | "2344444", 221 | "74763732", 222 | "4%", 223 | "/", 224 | ], 225 | vec!["none", "183176", "180", "182996", "1%", "/dev"], 226 | vec!["/dev/sda1", "233191", "17217", "203533", "8%", "/boot"], 227 | ]; 228 | 229 | let df = file_to_string(Path::new("fixtures/linux/disk_usage/df")).unwrap(); 230 | let disks = super::os::parse_df_output(&df).unwrap(); 231 | 232 | assert_eq!(expected, disks); 233 | } 234 | 235 | #[test] 236 | fn test_parse_df_i_output() { 237 | let expected = vec![ 238 | vec!["overlay", "2097152", "122591", "1974561", "6%", "/"], 239 | vec!["tmpfs", "254863", "16", "254847", "1%", "/dev"], 240 | vec!["tmpfs", "254863", "15", "254848", "1%", "/sys/fs/cgroup"], 241 | ]; 242 | 243 | let df = file_to_string(Path::new("fixtures/linux/disk_usage/df_i")).unwrap(); 244 | let disks = super::os::parse_df_output(&df).unwrap(); 245 | 246 | assert_eq!(expected, disks); 247 | } 248 | 249 | #[test] 250 | fn test_parse_df_output_incomplete() { 251 | let df = file_to_string(Path::new("fixtures/linux/disk_usage/df_incomplete")).unwrap(); 252 | match super::os::parse_df_output(&df) { 253 | Err(ProbeError::UnexpectedContent(_)) => (), 254 | r => panic!("Unexpected result: {:?}", r), 255 | } 256 | } 257 | 258 | #[test] 259 | fn test_parse_df_output_garbage() { 260 | let df = file_to_string(Path::new("fixtures/linux/disk_usage/df_garbage")).unwrap(); 261 | match super::os::parse_df_output(&df) { 262 | Err(ProbeError::UnexpectedContent(_)) => (), 263 | r => panic!("Unexpected result: {:?}", r), 264 | } 265 | } 266 | 267 | #[test] 268 | fn test_parse_df_i_output_dash_percentage() { 269 | let df = 270 | file_to_string(Path::new("fixtures/linux/disk_usage/df_i_dash_percentage")).unwrap(); 271 | let disks = 272 | super::os::parse_df_inodes_output(super::os::parse_df_output(&df).unwrap()).unwrap(); 273 | 274 | // Does not include the mountpoint with a dash (-) as a percentage 275 | assert_eq!( 276 | disks, 277 | vec![super::DiskInodeUsage { 278 | filesystem: Some("overlay".to_string()), 279 | inodes: 2097152, 280 | iused: 122591, 281 | ifree: 1974561, 282 | iused_percentage: 6, 283 | mountpoint: "/".to_string(), 284 | }] 285 | ); 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::error; 2 | use std::fmt; 3 | use std::io; 4 | 5 | #[derive(Debug)] 6 | pub enum ProbeError { 7 | /// IO error when opening file or command described in 8 | /// second field of the error 9 | IO(io::Error, String), 10 | /// Unexpected content in file or output 11 | UnexpectedContent(String), 12 | /// Input into a calculation function is invalid 13 | InvalidInput(String), 14 | /// Command failed 15 | StatusFailure(String), 16 | } 17 | 18 | impl fmt::Display for ProbeError { 19 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 20 | match *self { 21 | ProbeError::IO(ref err, ref path) => write!(f, "{} for {}", err, path), 22 | ProbeError::UnexpectedContent(ref err) => write!(f, "{}", err), 23 | ProbeError::InvalidInput(ref err) => write!(f, "{}", err), 24 | ProbeError::StatusFailure(ref err) => write!(f, "{}", err), 25 | } 26 | } 27 | } 28 | 29 | impl error::Error for ProbeError { 30 | fn description(&self) -> &str { 31 | match *self { 32 | #[allow(deprecated)] 33 | ProbeError::IO(ref err, ref _path) => err.description(), 34 | ProbeError::UnexpectedContent(ref err) => err, 35 | ProbeError::InvalidInput(ref err) => err, 36 | ProbeError::StatusFailure(ref err) => err, 37 | } 38 | } 39 | 40 | fn cause(&self) -> Option<&dyn error::Error> { 41 | match *self { 42 | ProbeError::IO(ref err, ref _path) => Some(err), 43 | ProbeError::UnexpectedContent(_) => None, 44 | ProbeError::InvalidInput(_) => None, 45 | ProbeError::StatusFailure(_) => None, 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate libc; 2 | 3 | pub mod cpu; 4 | pub mod disk_stats; 5 | pub mod disk_usage; 6 | mod error; 7 | pub mod load; 8 | pub mod memory; 9 | pub mod network; 10 | pub mod process_memory; 11 | 12 | use std::fs; 13 | use std::io; 14 | use std::io::BufRead; 15 | use std::io::Read; 16 | use std::path::Path; 17 | use std::result; 18 | use std::time::SystemTime; 19 | 20 | pub use crate::error::ProbeError; 21 | 22 | pub type Result = result::Result; 23 | 24 | #[inline] 25 | fn file_to_string(path: &Path) -> Result { 26 | let mut file = fs::File::open(path).map_err(|e| ProbeError::IO(e, path_to_string(path)))?; 27 | let mut read = String::new(); 28 | file.read_to_string(&mut read) 29 | .map_err(|e| ProbeError::IO(e, path_to_string(path)))?; 30 | Ok(read) 31 | } 32 | 33 | #[inline] 34 | fn file_to_buf_reader(path: &Path) -> Result> { 35 | fs::File::open(path) 36 | .map_err(|e| ProbeError::IO(e, path_to_string(path))) 37 | .and_then(|f| Ok(io::BufReader::new(f))) 38 | } 39 | 40 | #[inline] 41 | fn path_to_string(path: &Path) -> String { 42 | path.to_string_lossy().to_string() 43 | } 44 | 45 | #[inline] 46 | fn calculate_time_difference(first_time: u64, second_time: u64) -> Result { 47 | if first_time > second_time { 48 | Err(ProbeError::InvalidInput(format!( 49 | "first time {} was after second time {}", 50 | first_time, second_time 51 | ))) 52 | } else { 53 | Ok(second_time - first_time) 54 | } 55 | } 56 | 57 | #[inline] 58 | fn time_adjusted( 59 | field_name: &str, 60 | first_value: u64, 61 | second_value: u64, 62 | time_difference_ns: u64, 63 | ) -> Result { 64 | if first_value < second_value { 65 | Err(ProbeError::UnexpectedContent(format!( 66 | "First value {} was lower than second value {} for '{}'", 67 | first_value, second_value, field_name 68 | ))) 69 | } else { 70 | Ok( 71 | ((first_value - second_value) as f64 / time_difference_ns as f64 * 60_000_000_000.0) 72 | as u64, 73 | ) 74 | } 75 | } 76 | 77 | #[inline] 78 | fn parse_u64(segment: &str) -> Result { 79 | segment 80 | .parse() 81 | .map_err(|_| ProbeError::UnexpectedContent(format!("Could not parse '{}' as u64", segment))) 82 | } 83 | 84 | #[inline] 85 | fn dir_exists(path: &Path) -> bool { 86 | path.exists() && path.is_dir() 87 | } 88 | 89 | #[inline] 90 | fn read_file_value_as_u64(path: &Path) -> Result { 91 | let mut reader = file_to_buf_reader(path)?; 92 | let mut line = String::new(); 93 | reader 94 | .read_line(&mut line) 95 | .map_err(|e| ProbeError::IO(e, path_to_string(path)))?; 96 | parse_u64(&line.trim()) 97 | } 98 | 99 | #[inline] 100 | fn precise_time_ns() -> u64 { 101 | return SystemTime::now() 102 | .duration_since(SystemTime::UNIX_EPOCH) 103 | .unwrap() 104 | .as_nanos() as u64; 105 | } 106 | 107 | fn bytes_to_kilo_bytes(bytes: u64) -> u64 { 108 | bytes.checked_div(1024).unwrap_or(0) 109 | } 110 | 111 | #[cfg(test)] 112 | mod tests { 113 | use crate::error::ProbeError; 114 | 115 | #[test] 116 | fn test_calculate_time_difference() { 117 | assert_eq!(100, super::calculate_time_difference(100, 200).unwrap()); 118 | assert!(super::calculate_time_difference(200, 100).is_err()); 119 | } 120 | 121 | #[test] 122 | fn test_time_adjusted() { 123 | assert_eq!( 124 | 1200, 125 | super::time_adjusted("field", 2400, 1200, 60_000_000_000).unwrap() 126 | ); 127 | assert_eq!( 128 | 2400, 129 | super::time_adjusted("field", 2400, 1200, 30_000_000_000).unwrap() 130 | ); 131 | assert_eq!( 132 | 4800, 133 | super::time_adjusted("field", 2400, 1200, 15_000_000_000).unwrap() 134 | ); 135 | } 136 | 137 | #[test] 138 | fn test_time_adjusted_first_higher_than_lower() { 139 | match super::time_adjusted("field", 1200, 2400, 60_000_000_000) { 140 | Err(ProbeError::UnexpectedContent(_)) => (), 141 | r => panic!("Unexpected result: {:?}", r), 142 | } 143 | } 144 | 145 | #[test] 146 | fn test_parse_u64() { 147 | assert_eq!(100, super::parse_u64("100").unwrap()); 148 | assert!(super::parse_u64("something").is_err()); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/load.rs: -------------------------------------------------------------------------------- 1 | use super::Result; 2 | 3 | #[derive(Debug, PartialEq)] 4 | pub struct LoadAverage { 5 | pub one: f32, 6 | pub five: f32, 7 | pub fifteen: f32, 8 | } 9 | 10 | /// Read the current load average of the system. 11 | #[cfg(target_os = "linux")] 12 | pub fn read() -> Result { 13 | os::read() 14 | } 15 | 16 | #[cfg(target_os = "linux")] 17 | mod os { 18 | use std::path::Path; 19 | 20 | use super::super::file_to_string; 21 | use super::super::ProbeError; 22 | use super::super::Result; 23 | use super::LoadAverage; 24 | 25 | #[inline] 26 | pub fn read() -> Result { 27 | read_and_parse_load_average(&Path::new("/proc/loadavg")) 28 | } 29 | 30 | #[inline] 31 | pub fn read_and_parse_load_average(path: &Path) -> Result { 32 | let raw_data = file_to_string(path)?; 33 | let segments: Vec<&str> = raw_data.split_whitespace().collect(); 34 | 35 | if segments.len() < 3 { 36 | return Err(ProbeError::UnexpectedContent( 37 | "Incorrect number of segments".to_owned(), 38 | )); 39 | } 40 | 41 | Ok(LoadAverage { 42 | one: parse_segment(segments[0])?, 43 | five: parse_segment(segments[1])?, 44 | fifteen: parse_segment(segments[2])?, 45 | }) 46 | } 47 | 48 | #[inline] 49 | fn parse_segment(segment: &str) -> Result { 50 | segment 51 | .parse() 52 | .map_err(|_| ProbeError::UnexpectedContent("Could not parse segment".to_owned())) 53 | } 54 | } 55 | 56 | #[cfg(test)] 57 | #[cfg(target_os = "linux")] 58 | mod tests { 59 | use super::super::ProbeError; 60 | use super::LoadAverage; 61 | use std::path::Path; 62 | 63 | #[test] 64 | fn test_read_load_average() { 65 | assert!(super::read().is_ok()); 66 | } 67 | 68 | #[test] 69 | fn test_read_and_parse_load_average() { 70 | let path = Path::new("fixtures/linux/load/proc_loadavg"); 71 | let load_average = super::os::read_and_parse_load_average(&path).unwrap(); 72 | 73 | let expected = LoadAverage { 74 | one: 0.01, 75 | five: 0.02, 76 | fifteen: 0.03, 77 | }; 78 | 79 | assert_eq!(expected, load_average); 80 | } 81 | 82 | #[test] 83 | fn test_read_and_parse_load_average_wrong_path() { 84 | let path = Path::new("/nonsense"); 85 | match super::os::read_and_parse_load_average(&path) { 86 | Err(ProbeError::IO(_, _)) => (), 87 | r => panic!("Unexpected result: {:?}", r), 88 | } 89 | } 90 | 91 | #[test] 92 | fn test_read_and_parse_load_average_incomplete() { 93 | let path = Path::new("fixtures/linux/load/proc_loadavg_incomplete"); 94 | match super::os::read_and_parse_load_average(&path) { 95 | Err(ProbeError::UnexpectedContent(_)) => (), 96 | r => panic!("Unexpected result: {:?}", r), 97 | } 98 | } 99 | 100 | #[test] 101 | fn test_read_and_parse_load_average_garbage() { 102 | let path = Path::new("fixtures/linux/load/proc_loadavg_garbage"); 103 | match super::os::read_and_parse_load_average(&path) { 104 | Err(ProbeError::UnexpectedContent(_)) => (), 105 | r => panic!("Unexpected result: {:?}", r), 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/memory/cgroup.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use super::Memory; 4 | use crate::{dir_exists, ProbeError, Result}; 5 | 6 | /// Read the current memory status of the container. 7 | #[cfg(target_os = "linux")] 8 | pub fn read() -> Result { 9 | use super::cgroup_v1::read_and_parse_v1_sys_memory; 10 | use super::cgroup_v2::read_and_parse_v2_sys_memory; 11 | 12 | let v2_sys_fs_dir = Path::new("/sys/fs/cgroup"); 13 | let v2_sys_fs_file = v2_sys_fs_dir.join("memory.current"); 14 | 15 | if v2_sys_fs_file.exists() { 16 | return read_and_parse_v2_sys_memory(&v2_sys_fs_dir); 17 | } 18 | 19 | let v1_sys_fs_dir = Path::new("/sys/fs/cgroup/memory/"); 20 | if dir_exists(v1_sys_fs_dir) { 21 | return read_and_parse_v1_sys_memory(&v1_sys_fs_dir); 22 | } 23 | 24 | let message = format!( 25 | "Directory `{}` not found", 26 | v1_sys_fs_dir.to_str().unwrap_or("unknown path") 27 | ); 28 | Err(ProbeError::UnexpectedContent(message)) 29 | } 30 | 31 | #[cfg(test)] 32 | #[cfg(target_os = "linux")] 33 | mod tests { 34 | #[test] 35 | fn test_read_from_container() { 36 | assert!(super::read().is_ok()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/memory/cgroup_v1.rs: -------------------------------------------------------------------------------- 1 | use std::io::BufRead; 2 | use std::path::Path; 3 | 4 | use super::Memory; 5 | use crate::{bytes_to_kilo_bytes, file_to_buf_reader, parse_u64, read_file_value_as_u64}; 6 | use crate::{path_to_string, ProbeError, Result}; 7 | 8 | #[cfg(target_os = "linux")] 9 | pub fn read_and_parse_v1_sys_memory(path: &Path) -> Result { 10 | let mut memory = Memory { 11 | total: None, 12 | free: None, 13 | used: 0, 14 | buffers: None, 15 | cached: None, 16 | shmem: None, 17 | swap_total: None, 18 | swap_free: None, 19 | swap_used: None, 20 | }; 21 | 22 | let limit = read_file_value_as_u64(&path.join("memory.limit_in_bytes"))?; 23 | // Ignore number reported by cgroups v1 when no limit is set 24 | if limit < 9223372036854771712u64 { 25 | memory.total = Some(bytes_to_kilo_bytes(limit)); 26 | } 27 | 28 | let used_memory = 29 | bytes_to_kilo_bytes(read_file_value_as_u64(&path.join("memory.usage_in_bytes"))?); 30 | 31 | let reader = file_to_buf_reader(&path.join("memory.stat"))?; 32 | for line_result in reader.lines() { 33 | let line = line_result.map_err(|e| ProbeError::IO(e, path_to_string(path)))?; 34 | let segments: Vec<&str> = line.split_whitespace().collect(); 35 | if let Ok(value) = parse_u64(&segments[1]) { 36 | match segments[0] { 37 | "shmem" => { 38 | memory.shmem = Some(bytes_to_kilo_bytes(value)); 39 | } 40 | "cache" => { 41 | memory.cached = Some(bytes_to_kilo_bytes(value)); 42 | } 43 | _ => (), 44 | }; 45 | } 46 | } 47 | memory.used = used_memory - memory.cached.unwrap_or(0); 48 | memory.free = memory.total.map(|total| total - memory.used); 49 | 50 | memory.swap_total = match read_file_value_as_u64(&path.join("memory.memsw.limit_in_bytes")) { 51 | Ok(value) => memory 52 | .total 53 | .map(|total| bytes_to_kilo_bytes(value).saturating_sub(total)), 54 | Err(_) => None, 55 | }; 56 | memory.swap_used = match read_file_value_as_u64(&path.join("memory.memsw.usage_in_bytes")) { 57 | Ok(value) => Some(bytes_to_kilo_bytes(value).saturating_sub(used_memory)), 58 | Err(_) => None, 59 | }; 60 | memory.swap_free = memory 61 | .swap_total 62 | .zip(memory.swap_used) 63 | .map(|(total, used)| total.saturating_sub(used)); 64 | 65 | Ok(memory) 66 | } 67 | 68 | #[cfg(test)] 69 | #[cfg(target_os = "linux")] 70 | mod tests { 71 | use super::super::Memory; 72 | use crate::ProbeError; 73 | use std::path::Path; 74 | 75 | #[test] 76 | fn test_read_and_parse_v1_sys_memory() { 77 | let path = Path::new("fixtures/linux/sys/fs/cgroup_v1/memory/"); 78 | let memory = super::read_and_parse_v1_sys_memory(&path).unwrap(); 79 | 80 | let expected = Memory { 81 | total: Some(512000), // 500mb 82 | free: Some(503400), // total - used 83 | used: 8600, 84 | buffers: None, 85 | cached: Some(58928), 86 | shmem: None, 87 | swap_total: Some(1_488_000), // reported swap total - reported memory total 88 | swap_free: Some(1_055_528), 89 | swap_used: Some(432_472), // reported swap used - (reported memory used, including cache) 90 | }; 91 | assert_eq!(expected, memory); 92 | assert_eq!(memory.total.unwrap(), memory.used + memory.free.unwrap()); 93 | assert_eq!( 94 | memory.swap_total.unwrap(), 95 | memory.swap_used.unwrap() + memory.swap_free.unwrap() 96 | ); 97 | } 98 | 99 | #[test] 100 | fn test_read_and_parse_v1_sys_memory_wrong_path() { 101 | let path = Path::new("/nonsense"); 102 | match super::read_and_parse_v1_sys_memory(&path) { 103 | Err(ProbeError::IO(_, _)) => (), 104 | r => panic!("Unexpected result: {:?}", r), 105 | } 106 | } 107 | 108 | #[test] 109 | fn test_read_and_parse_v1_sys_memory_incomplete() { 110 | let path = Path::new("fixtures/linux/sys/fs/cgroup_v1/memory_incomplete/"); 111 | match super::read_and_parse_v1_sys_memory(&path) { 112 | Err(ProbeError::UnexpectedContent(_)) => (), 113 | r => panic!("Unexpected result: {:?}", r), 114 | } 115 | } 116 | 117 | #[test] 118 | fn test_read_and_parse_v1_sys_memory_missing_files() { 119 | let path = Path::new("fixtures/linux/sys/fs/cgroup_v1/memory_missing_files/"); 120 | match super::read_and_parse_v1_sys_memory(&path) { 121 | Err(ProbeError::IO(_, _)) => (), 122 | r => panic!("Unexpected result: {:?}", r), 123 | } 124 | } 125 | 126 | #[test] 127 | fn test_read_and_parse_v1_sys_memory_garbage() { 128 | let path = Path::new("fixtures/linux/sys/fs/cgroup_v1/memory_garbage/"); 129 | match super::read_and_parse_v1_sys_memory(&path) { 130 | Err(ProbeError::UnexpectedContent(_)) => (), 131 | r => panic!("Unexpected result: {:?}", r), 132 | } 133 | } 134 | 135 | #[test] 136 | fn test_read_and_parse_v1_sys_memory_no_swap() { 137 | let path = Path::new("fixtures/linux/sys/fs/cgroup_v1/memory_without_swap/"); 138 | let memory = super::read_and_parse_v1_sys_memory(&path).unwrap(); 139 | 140 | let expected = Memory { 141 | total: Some(512000), // 500mb 142 | free: Some(503400), // total - used 143 | used: 8600, 144 | buffers: None, 145 | cached: Some(58928), 146 | shmem: None, 147 | swap_total: None, // Reads 0 swap 148 | swap_free: None, // Reads 0 swap 149 | swap_used: None, 150 | }; 151 | assert_eq!(expected, memory); 152 | assert_eq!(memory.total.unwrap(), memory.used + memory.free.unwrap()); 153 | assert_eq!(memory.swap_total, None); 154 | assert_eq!(memory.swap_free, None); 155 | assert_eq!(memory.swap_used, None); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/memory/cgroup_v2.rs: -------------------------------------------------------------------------------- 1 | use std::io::BufRead; 2 | use std::path::Path; 3 | 4 | use super::Memory; 5 | use crate::{bytes_to_kilo_bytes, file_to_buf_reader, parse_u64, read_file_value_as_u64}; 6 | use crate::{path_to_string, ProbeError, Result}; 7 | #[cfg(target_os = "linux")] 8 | pub fn read_and_parse_v2_sys_memory(path: &Path) -> Result { 9 | let mut memory = Memory { 10 | total: None, 11 | free: None, 12 | used: 0, 13 | buffers: None, 14 | cached: None, 15 | shmem: None, 16 | swap_total: None, 17 | swap_free: None, 18 | swap_used: None, 19 | }; 20 | 21 | memory.total = read_file_value_as_u64(&path.join("memory.max")) 22 | .ok() 23 | .map(bytes_to_kilo_bytes); 24 | 25 | memory.used = bytes_to_kilo_bytes(read_file_value_as_u64(&path.join("memory.current"))?); 26 | 27 | let reader = file_to_buf_reader(&path.join("memory.stat"))?; 28 | for line_result in reader.lines() { 29 | let line = line_result.map_err(|e| ProbeError::IO(e, path_to_string(path)))?; 30 | let segments: Vec<&str> = line.split_whitespace().collect(); 31 | let value = parse_u64(&segments[1])?; 32 | 33 | if segments[0] == "shmem" { 34 | memory.shmem = Some(bytes_to_kilo_bytes(value)); 35 | break; 36 | }; 37 | } 38 | 39 | memory.free = memory.total.map(|total| total - memory.used); 40 | 41 | memory.swap_total = read_file_value_as_u64(&path.join("memory.swap.max")) 42 | .ok() 43 | .map(bytes_to_kilo_bytes); 44 | memory.swap_used = read_file_value_as_u64(&path.join("memory.swap.current")) 45 | .ok() 46 | .map(bytes_to_kilo_bytes); 47 | memory.swap_free = memory 48 | .swap_total 49 | .zip(memory.swap_used) 50 | .map(|(total, used)| total.saturating_sub(used)); 51 | 52 | Ok(memory) 53 | } 54 | 55 | #[cfg(test)] 56 | #[cfg(target_os = "linux")] 57 | mod tests { 58 | use super::super::Memory; 59 | use crate::ProbeError; 60 | use std::path::Path; 61 | 62 | #[test] 63 | fn test_read_and_parse_v2_sys_memory() { 64 | let path = Path::new("fixtures/linux/sys/fs/cgroup_v2/memory/"); 65 | let memory = super::read_and_parse_v2_sys_memory(&path).unwrap(); 66 | 67 | let expected = Memory { 68 | total: Some(512000), // 500mb 69 | free: Some(444472), // total - used 70 | used: 67528, 71 | buffers: None, 72 | cached: None, 73 | shmem: Some(0), 74 | swap_total: Some(2000000), // reported swap total 75 | swap_free: Some(1_500_000), // swap total - swap used 76 | swap_used: Some(500_000), // reported swap used 77 | }; 78 | assert_eq!(expected, memory); 79 | assert_eq!(memory.total.unwrap(), memory.used + memory.free.unwrap()); 80 | assert_eq!( 81 | memory.swap_total.unwrap(), 82 | memory.swap_used.unwrap() + memory.swap_free.unwrap() 83 | ); 84 | } 85 | 86 | #[test] 87 | fn test_read_and_parse_v2_sys_memory_wrong_path() { 88 | let path = Path::new("/nonsense"); 89 | match super::read_and_parse_v2_sys_memory(&path) { 90 | Err(ProbeError::IO(_, _)) => (), 91 | r => panic!("Unexpected result: {:?}", r), 92 | } 93 | } 94 | 95 | #[test] 96 | fn test_read_and_parse_v2_sys_memory_incomplete() { 97 | let path = Path::new("fixtures/linux/sys/fs/cgroup_v2/memory_incomplete/"); 98 | match super::read_and_parse_v2_sys_memory(&path) { 99 | Err(ProbeError::UnexpectedContent(_)) => (), 100 | r => panic!("Unexpected result: {:?}", r), 101 | } 102 | } 103 | 104 | #[test] 105 | fn test_read_and_parse_v1_sys_memory_missing_files() { 106 | let path = Path::new("fixtures/linux/sys/fs/cgroup_v2/memory_missing_files/"); 107 | match super::read_and_parse_v2_sys_memory(&path) { 108 | Err(ProbeError::IO(_, _)) => (), 109 | r => panic!("Unexpected result: {:?}", r), 110 | } 111 | } 112 | 113 | #[test] 114 | fn test_read_and_parse_v1_sys_memory_garbage() { 115 | let path = Path::new("fixtures/linux/sys/fs/cgroup_v2/memory_garbage/"); 116 | match super::read_and_parse_v2_sys_memory(&path) { 117 | Err(ProbeError::UnexpectedContent(_)) => (), 118 | r => panic!("Unexpected result: {:?}", r), 119 | } 120 | } 121 | 122 | #[test] 123 | fn test_read_and_parse_v1_sys_memory_no_swap() { 124 | let path = Path::new("fixtures/linux/sys/fs/cgroup_v2/memory_without_swap/"); 125 | let memory = super::read_and_parse_v2_sys_memory(&path).unwrap(); 126 | 127 | let expected = Memory { 128 | total: Some(512000), // 500mb 129 | free: Some(444472), // total - used 130 | used: 67528, 131 | buffers: None, 132 | cached: None, 133 | shmem: Some(0), 134 | swap_total: None, // Reads 0 swap 135 | swap_free: None, // Reads 0 swap 136 | swap_used: None, 137 | }; 138 | assert_eq!(expected, memory); 139 | assert_eq!(memory.total.unwrap(), memory.used + memory.free.unwrap()); 140 | assert_eq!(memory.swap_total, None); 141 | assert_eq!(memory.swap_free, None); 142 | assert_eq!(memory.swap_used, None); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/memory/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cgroup; 2 | mod cgroup_v1; 3 | mod cgroup_v2; 4 | pub mod proc; 5 | 6 | #[derive(Debug, PartialEq)] 7 | pub struct Memory { 8 | pub total: Option, 9 | pub free: Option, 10 | pub used: u64, 11 | pub buffers: Option, 12 | pub cached: Option, 13 | pub shmem: Option, 14 | pub swap_total: Option, 15 | pub swap_free: Option, 16 | pub swap_used: Option, 17 | } 18 | -------------------------------------------------------------------------------- /src/memory/proc.rs: -------------------------------------------------------------------------------- 1 | use super::Memory; 2 | use crate::Result; 3 | 4 | /// Read the current memory status of the system. 5 | #[cfg(target_os = "linux")] 6 | pub fn read() -> Result { 7 | os::read() 8 | } 9 | 10 | #[cfg(target_os = "linux")] 11 | mod os { 12 | use std::io::BufRead; 13 | use std::path::Path; 14 | 15 | use super::super::Memory; 16 | use crate::{file_to_buf_reader, parse_u64}; 17 | use crate::{path_to_string, ProbeError, Result}; 18 | 19 | const PROC_MEMORY_NUMBER_OF_FIELDS: usize = 7; 20 | 21 | #[inline] 22 | pub fn read() -> Result { 23 | read_and_parse_proc_memory(&Path::new("/proc/meminfo")) 24 | } 25 | 26 | #[inline] 27 | pub fn read_and_parse_proc_memory(path: &Path) -> Result { 28 | let mut memory = Memory { 29 | total: None, 30 | free: None, 31 | used: 0, 32 | buffers: None, 33 | cached: None, 34 | shmem: None, 35 | swap_total: None, 36 | swap_free: None, 37 | swap_used: None, 38 | }; 39 | let mut free = 0; 40 | 41 | let reader = file_to_buf_reader(path)?; 42 | 43 | let mut fields_encountered = 0; 44 | for line_result in reader.lines() { 45 | let line = line_result.map_err(|e| ProbeError::IO(e, path_to_string(path)))?; 46 | let segments: Vec<&str> = line.split_whitespace().collect(); 47 | let value: u64 = parse_u64(segments[1])?; 48 | 49 | // If this is a field we recognize set it's value and increment the 50 | // number of fields we encountered. 51 | fields_encountered += match segments[0] { 52 | "MemTotal:" => { 53 | memory.total = Some(value); 54 | 1 55 | } 56 | "MemFree:" => { 57 | free = value; 58 | 1 59 | } 60 | "Buffers:" => { 61 | memory.buffers = Some(value); 62 | 1 63 | } 64 | "Cached:" => { 65 | memory.cached = Some(value); 66 | 1 67 | } 68 | "SwapTotal:" => { 69 | memory.swap_total = Some(value); 70 | 1 71 | } 72 | "SwapFree:" => { 73 | memory.swap_free = Some(value); 74 | 1 75 | } 76 | "Shmem:" => { 77 | memory.shmem = Some(value); 78 | 1 79 | } 80 | _ => 0, 81 | }; 82 | 83 | if fields_encountered == PROC_MEMORY_NUMBER_OF_FIELDS { 84 | break; 85 | } 86 | } 87 | 88 | if fields_encountered != PROC_MEMORY_NUMBER_OF_FIELDS || memory.total.is_none() { 89 | return Err(ProbeError::UnexpectedContent( 90 | "Did not encounter all expected fields".to_owned(), 91 | )); 92 | } 93 | 94 | // Total amount of free physical memory in Kb. 95 | // Includes buffers and caches, these will be freed 96 | // up by the OS when the memory is needed. 97 | memory.free = Some(free + memory.buffers.unwrap_or(0) + memory.cached.unwrap_or(0)); 98 | memory.used = memory.total.unwrap() - memory.free.unwrap(); 99 | memory.swap_used = memory 100 | .swap_total 101 | .zip(memory.swap_free) 102 | .map(|(total, free)| total - free); 103 | 104 | Ok(memory) 105 | } 106 | } 107 | 108 | #[cfg(test)] 109 | #[cfg(target_os = "linux")] 110 | mod tests { 111 | use super::super::Memory; 112 | use crate::ProbeError; 113 | use std::path::Path; 114 | 115 | #[test] 116 | fn test_read_memory() { 117 | assert!(super::read().is_ok()); 118 | } 119 | 120 | #[test] 121 | fn test_read_and_parse_proc_memory() { 122 | let path = Path::new("fixtures/linux/memory/proc_meminfo"); 123 | let memory = super::os::read_and_parse_proc_memory(&path).unwrap(); 124 | 125 | let expected = Memory { 126 | total: Some(376072), 127 | free: Some(324248), 128 | used: 51824, 129 | buffers: Some(22820), 130 | cached: Some(176324), 131 | shmem: Some(548), 132 | swap_total: Some(1101816), 133 | swap_free: Some(1100644), 134 | swap_used: Some(1172), 135 | }; 136 | assert_eq!(expected, memory); 137 | assert_eq!(memory.total.unwrap(), memory.used + memory.free.unwrap()); 138 | assert_eq!( 139 | memory.swap_total.unwrap(), 140 | memory.swap_used.unwrap() + memory.swap_free.unwrap() 141 | ); 142 | } 143 | 144 | #[test] 145 | fn test_read_and_parse_memory_wrong_path() { 146 | let path = Path::new("/nonsense"); 147 | match super::os::read_and_parse_proc_memory(&path) { 148 | Err(ProbeError::IO(_, _)) => (), 149 | r => panic!("Unexpected result: {:?}", r), 150 | } 151 | } 152 | 153 | #[test] 154 | fn test_read_and_parse_memory_incomplete() { 155 | let path = Path::new("fixtures/linux/memory/proc_meminfo_incomplete"); 156 | match super::os::read_and_parse_proc_memory(&path) { 157 | Err(ProbeError::UnexpectedContent(_)) => (), 158 | r => panic!("Unexpected result: {:?}", r), 159 | } 160 | } 161 | 162 | #[test] 163 | fn test_read_and_parse_memory_garbage() { 164 | let path = Path::new("fixtures/linux/memory/proc_meminfo_garbage"); 165 | match super::os::read_and_parse_proc_memory(&path) { 166 | Err(ProbeError::UnexpectedContent(_)) => (), 167 | r => panic!("Unexpected result: {:?}", r), 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/network.rs: -------------------------------------------------------------------------------- 1 | use super::{calculate_time_difference, ProbeError, Result}; 2 | use std::collections::HashMap; 3 | 4 | pub type Interfaces = HashMap; 5 | 6 | /// Measurement of network traffic at a certain time. 7 | #[derive(Debug, PartialEq)] 8 | pub struct NetworkTrafficMeasurement { 9 | pub precise_time_ns: u64, 10 | pub interfaces: Interfaces, 11 | } 12 | 13 | impl NetworkTrafficMeasurement { 14 | /// Calculate the network traffic per minute based on this measurement and a measurement in the 15 | /// future. It is advisable to make the next measurement roughly a minute from this one for the 16 | /// most reliable result. 17 | pub fn calculate_per_minute( 18 | &self, 19 | next_measurement: &NetworkTrafficMeasurement, 20 | ) -> Result { 21 | let time_difference = 22 | calculate_time_difference(self.precise_time_ns, next_measurement.precise_time_ns)?; 23 | 24 | let mut interfaces = Interfaces::new(); 25 | 26 | for (name, traffic) in self.interfaces.iter() { 27 | let next_traffic = match next_measurement.interfaces.get(name) { 28 | Some(interface) => interface, 29 | None => { 30 | return Err(ProbeError::UnexpectedContent(format!( 31 | "{} is not present in the next measurement", 32 | name 33 | ))) 34 | } 35 | }; 36 | interfaces.insert( 37 | name.to_string(), 38 | NetworkTraffic { 39 | received: super::time_adjusted( 40 | "received", 41 | next_traffic.received, 42 | traffic.received, 43 | time_difference, 44 | )?, 45 | transmitted: super::time_adjusted( 46 | "transmitted", 47 | next_traffic.transmitted, 48 | traffic.transmitted, 49 | time_difference, 50 | )?, 51 | }, 52 | ); 53 | } 54 | 55 | Ok(NetworkTrafficPerMinute { interfaces }) 56 | } 57 | } 58 | 59 | /// Network traffic in bytes. 60 | #[derive(Debug, PartialEq)] 61 | pub struct NetworkTraffic { 62 | pub received: u64, 63 | pub transmitted: u64, 64 | } 65 | 66 | /// Network traffic for a certain minute, calculated based on two measurements. 67 | #[derive(Debug, PartialEq)] 68 | pub struct NetworkTrafficPerMinute { 69 | pub interfaces: Interfaces, 70 | } 71 | 72 | #[cfg(target_os = "linux")] 73 | pub fn read() -> Result { 74 | os::read() 75 | } 76 | 77 | #[cfg(target_os = "linux")] 78 | mod os { 79 | use std::io::{self, BufRead}; 80 | use std::path::Path; 81 | 82 | use super::super::{file_to_buf_reader, parse_u64, path_to_string, precise_time_ns, Result}; 83 | use super::{Interfaces, NetworkTraffic, NetworkTrafficMeasurement}; 84 | use crate::error::ProbeError; 85 | 86 | #[inline] 87 | pub fn read() -> Result { 88 | read_and_parse_network(&Path::new("/proc/net/dev")) 89 | } 90 | 91 | #[inline] 92 | pub fn read_and_parse_network(path: &Path) -> Result { 93 | let reader = file_to_buf_reader(path)?; 94 | 95 | let precise_time_ns = precise_time_ns(); 96 | 97 | let line_result: io::Result> = reader.lines().collect(); 98 | let lines = line_result.map_err(|e| ProbeError::IO(e, path_to_string(path)))?; 99 | let positions = get_positions(lines[1].as_ref())?; 100 | 101 | let mut interfaces = Interfaces::new(); 102 | for line in &lines[2..] { 103 | let segments: Vec<&str> = line.split_whitespace().collect(); 104 | let name = segments[0].trim_matches(':').to_owned(); 105 | 106 | if segments.len() < positions.transmit_bytes { 107 | return Err(ProbeError::UnexpectedContent(format!( 108 | "Expected at least {} items, had {} for '{}'", 109 | positions.transmit_bytes, 110 | segments.len(), 111 | name 112 | ))); 113 | } 114 | 115 | let traffic = NetworkTraffic { 116 | received: parse_u64(segments[positions.receive_bytes])?, 117 | transmitted: parse_u64(segments[positions.transmit_bytes])?, 118 | }; 119 | 120 | interfaces.insert(name, traffic); 121 | } 122 | 123 | Ok(NetworkTrafficMeasurement { 124 | precise_time_ns, 125 | interfaces, 126 | }) 127 | } 128 | 129 | #[derive(Debug, PartialEq)] 130 | pub struct Positions { 131 | pub receive_bytes: usize, 132 | pub transmit_bytes: usize, 133 | } 134 | 135 | /// Get the positions of the `bytes` field for both the receive and transmit segment 136 | #[inline] 137 | pub fn get_positions(header_line: &str) -> Result { 138 | let groups: Vec<&str> = header_line.split('|').collect(); 139 | if groups.len() != 3 { 140 | return Err(ProbeError::UnexpectedContent( 141 | "Incorrect number of segments".to_owned(), 142 | )); 143 | } 144 | let receive_group: Vec<&str> = groups[1].split_whitespace().collect(); 145 | let transmit_group: Vec<&str> = groups[2].split_whitespace().collect(); 146 | 147 | let receive_pos = receive_group 148 | .iter() 149 | .position(|&e| e == "bytes") 150 | .ok_or_else(|| { 151 | ProbeError::UnexpectedContent("bytes field not found for receive".to_string()) 152 | })?; 153 | let transmit_pos = transmit_group 154 | .iter() 155 | .position(|&e| e == "bytes") 156 | .ok_or_else(|| { 157 | ProbeError::UnexpectedContent("bytes field not found for transmit".to_string()) 158 | })?; 159 | 160 | // We start with 1 here because the first (name) segment always has one column. 161 | Ok(Positions { 162 | receive_bytes: 1 + receive_pos, 163 | transmit_bytes: 1 + receive_group.len() + transmit_pos, 164 | }) 165 | } 166 | } 167 | 168 | #[cfg(test)] 169 | #[cfg(target_os = "linux")] 170 | mod tests { 171 | use super::super::{precise_time_ns, ProbeError}; 172 | use super::{Interfaces, NetworkTraffic, NetworkTrafficMeasurement}; 173 | use std::path::Path; 174 | 175 | #[test] 176 | fn test_read_network() { 177 | assert!(super::read().is_ok()); 178 | assert!(!super::read().unwrap().interfaces.is_empty()); 179 | } 180 | 181 | #[test] 182 | fn test_read_and_parse_network() { 183 | let path = Path::new("fixtures/linux/network/proc_net_dev"); 184 | let measurement = super::os::read_and_parse_network(&path).unwrap(); 185 | 186 | assert!(measurement.precise_time_ns < precise_time_ns()); 187 | 188 | let interfaces = measurement.interfaces; 189 | assert_eq!(3, interfaces.len()); 190 | 191 | let lo = interfaces.get("lo").unwrap(); 192 | assert_eq!(560, lo.received); 193 | assert_eq!(560, lo.transmitted); 194 | 195 | let eth0 = interfaces.get("eth0").unwrap(); 196 | assert_eq!(254972, eth0.received); 197 | assert_eq!(72219, eth0.transmitted); 198 | 199 | let eth1 = interfaces.get("eth1").unwrap(); 200 | assert_eq!(354972, eth1.received); 201 | assert_eq!(82219, eth1.transmitted); 202 | } 203 | 204 | #[test] 205 | fn test_read_and_parse_network_wrong_path() { 206 | let path = Path::new("/nonsense"); 207 | match super::os::read_and_parse_network(&path) { 208 | Err(ProbeError::IO(_, _)) => (), 209 | r => panic!("Unexpected result: {:?}", r), 210 | } 211 | } 212 | 213 | #[test] 214 | fn test_read_and_parse_network_incomplete() { 215 | let path = Path::new("fixtures/linux/network/proc_net_dev_incomplete"); 216 | match super::os::read_and_parse_network(&path) { 217 | Err(ProbeError::UnexpectedContent(_)) => (), 218 | r => panic!("Unexpected result: {:?}", r), 219 | } 220 | } 221 | 222 | #[test] 223 | fn test_read_and_parse_network_garbage() { 224 | let path = Path::new("fixtures/linux/network/proc_net_dev_garbage"); 225 | match super::os::read_and_parse_network(&path) { 226 | Err(ProbeError::UnexpectedContent(_)) => (), 227 | r => panic!("Unexpected result: {:?}", r), 228 | } 229 | } 230 | 231 | #[test] 232 | fn test_get_positions() { 233 | let line = "face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed"; 234 | 235 | assert_eq!( 236 | super::os::Positions { 237 | receive_bytes: 1, 238 | transmit_bytes: 9 239 | }, 240 | super::os::get_positions(line).unwrap() 241 | ) 242 | } 243 | 244 | #[test] 245 | fn test_get_positions_fields_missing() { 246 | let line = "face"; 247 | 248 | match super::os::get_positions(line) { 249 | Err(ProbeError::UnexpectedContent(_)) => (), 250 | r => panic!("Unexpected result: {:?}", r), 251 | } 252 | } 253 | 254 | #[test] 255 | fn test_get_positions_bytes_field_missing() { 256 | let line = "face |bates packets errs drop fifo frame compressed multicast|bates packets errs drop fifo colls carrier compressed"; 257 | 258 | match super::os::get_positions(line) { 259 | Err(ProbeError::UnexpectedContent(_)) => (), 260 | r => panic!("Unexpected result: {:?}", r), 261 | } 262 | } 263 | 264 | #[test] 265 | fn test_calculate_per_minute_full_minute() { 266 | let mut interfaces1 = Interfaces::new(); 267 | interfaces1.insert( 268 | "eth0".to_string(), 269 | NetworkTraffic { 270 | received: 1000, 271 | transmitted: 1000, 272 | }, 273 | ); 274 | interfaces1.insert( 275 | "eth1".to_string(), 276 | NetworkTraffic { 277 | received: 2000, 278 | transmitted: 3000, 279 | }, 280 | ); 281 | let measurement1 = NetworkTrafficMeasurement { 282 | precise_time_ns: 60_000_000_000, 283 | interfaces: interfaces1, 284 | }; 285 | 286 | let mut interfaces2 = Interfaces::new(); 287 | interfaces2.insert( 288 | "eth0".to_string(), 289 | NetworkTraffic { 290 | received: 2000, 291 | transmitted: 2600, 292 | }, 293 | ); 294 | interfaces2.insert( 295 | "eth1".to_string(), 296 | NetworkTraffic { 297 | received: 3000, 298 | transmitted: 4600, 299 | }, 300 | ); 301 | let measurement2 = NetworkTrafficMeasurement { 302 | precise_time_ns: 120_000_000_000, 303 | interfaces: interfaces2, 304 | }; 305 | 306 | let per_minute = measurement1.calculate_per_minute(&measurement2).unwrap(); 307 | assert_eq!(2, per_minute.interfaces.len()); 308 | 309 | let eth0 = per_minute.interfaces.get("eth0").unwrap(); 310 | assert_eq!(1000, eth0.received); 311 | assert_eq!(1600, eth0.transmitted); 312 | 313 | let eth1 = per_minute.interfaces.get("eth0").unwrap(); 314 | assert_eq!(1000, eth1.received); 315 | assert_eq!(1600, eth1.transmitted); 316 | } 317 | 318 | #[test] 319 | fn test_calculate_per_minute_partial_minute() { 320 | let mut interfaces1 = Interfaces::new(); 321 | interfaces1.insert( 322 | "eth0".to_string(), 323 | NetworkTraffic { 324 | received: 1000, 325 | transmitted: 1000, 326 | }, 327 | ); 328 | interfaces1.insert( 329 | "eth1".to_string(), 330 | NetworkTraffic { 331 | received: 2000, 332 | transmitted: 3000, 333 | }, 334 | ); 335 | let measurement1 = NetworkTrafficMeasurement { 336 | precise_time_ns: 60_000_000_000, 337 | interfaces: interfaces1, 338 | }; 339 | 340 | let mut interfaces2 = Interfaces::new(); 341 | interfaces2.insert( 342 | "eth0".to_string(), 343 | NetworkTraffic { 344 | received: 2000, 345 | transmitted: 2600, 346 | }, 347 | ); 348 | interfaces2.insert( 349 | "eth1".to_string(), 350 | NetworkTraffic { 351 | received: 3000, 352 | transmitted: 4600, 353 | }, 354 | ); 355 | let measurement2 = NetworkTrafficMeasurement { 356 | precise_time_ns: 90_000_000_000, 357 | interfaces: interfaces2, 358 | }; 359 | 360 | let per_minute = measurement1.calculate_per_minute(&measurement2).unwrap(); 361 | assert_eq!(2, per_minute.interfaces.len()); 362 | 363 | let eth0 = per_minute.interfaces.get("eth0").unwrap(); 364 | assert_eq!(2000, eth0.received); 365 | assert_eq!(3200, eth0.transmitted); 366 | 367 | let eth1 = per_minute.interfaces.get("eth0").unwrap(); 368 | assert_eq!(2000, eth1.received); 369 | assert_eq!(3200, eth1.transmitted); 370 | } 371 | 372 | #[test] 373 | fn test_calculate_per_minute_wrong_times() { 374 | let measurement1 = NetworkTrafficMeasurement { 375 | precise_time_ns: 90_000_000_000, 376 | interfaces: Interfaces::new(), 377 | }; 378 | 379 | let measurement2 = NetworkTrafficMeasurement { 380 | precise_time_ns: 60_000_000_000, 381 | interfaces: Interfaces::new(), 382 | }; 383 | 384 | match measurement1.calculate_per_minute(&measurement2) { 385 | Err(ProbeError::InvalidInput(_)) => (), 386 | r => panic!("Unexpected result: {:?}", r), 387 | } 388 | } 389 | 390 | #[test] 391 | fn test_calculate_per_minute_values_lower() { 392 | let mut interfaces1 = Interfaces::new(); 393 | interfaces1.insert( 394 | "eth0".to_string(), 395 | NetworkTraffic { 396 | received: 2000, 397 | transmitted: 3000, 398 | }, 399 | ); 400 | let measurement1 = NetworkTrafficMeasurement { 401 | precise_time_ns: 60_000_000_000, 402 | interfaces: interfaces1, 403 | }; 404 | 405 | let mut interfaces2 = Interfaces::new(); 406 | interfaces2.insert( 407 | "eth0".to_string(), 408 | NetworkTraffic { 409 | received: 2000, 410 | transmitted: 2600, 411 | }, 412 | ); 413 | let measurement2 = NetworkTrafficMeasurement { 414 | precise_time_ns: 120_000_000_000, 415 | interfaces: interfaces2, 416 | }; 417 | 418 | match measurement1.calculate_per_minute(&measurement2) { 419 | Err(ProbeError::UnexpectedContent(_)) => (), 420 | r => panic!("Unexpected result: {:?}", r), 421 | } 422 | } 423 | 424 | #[test] 425 | fn test_calculate_per_minute_different_interfaces() { 426 | let mut interfaces1 = Interfaces::new(); 427 | interfaces1.insert( 428 | "eth1".to_string(), 429 | NetworkTraffic { 430 | received: 2000, 431 | transmitted: 3000, 432 | }, 433 | ); 434 | let measurement1 = NetworkTrafficMeasurement { 435 | precise_time_ns: 60_000_000_000, 436 | interfaces: interfaces1, 437 | }; 438 | 439 | let mut interfaces2 = Interfaces::new(); 440 | interfaces2.insert( 441 | "eth0".to_string(), 442 | NetworkTraffic { 443 | received: 2000, 444 | transmitted: 2600, 445 | }, 446 | ); 447 | let measurement2 = NetworkTrafficMeasurement { 448 | precise_time_ns: 120_000_000_000, 449 | interfaces: interfaces2, 450 | }; 451 | 452 | match measurement1.calculate_per_minute(&measurement2) { 453 | Err(ProbeError::UnexpectedContent(_)) => (), 454 | r => panic!("Unexpected result: {:?}", r), 455 | } 456 | } 457 | } 458 | -------------------------------------------------------------------------------- /src/process_memory.rs: -------------------------------------------------------------------------------- 1 | // These methods are described in: 2 | // http://nadeausoftware.com/articles/2012/07/c_c_tip_how_get_process_resident_set_size_physical_memory_use 3 | 4 | use super::Result; 5 | 6 | /// Get the current RSS memory of this process in KB 7 | #[cfg(target_os = "linux")] 8 | pub fn current_rss() -> Result { 9 | os::current_rss() 10 | } 11 | 12 | /// Get the current RSS memory of a process with given pid in KB 13 | #[cfg(target_os = "linux")] 14 | pub fn current_rss_of(pid: libc::pid_t) -> Result { 15 | os::current_rss_of(pid) 16 | } 17 | 18 | /// Get the max RSS memory of this process in KB 19 | #[cfg(target_os = "linux")] 20 | pub fn max_rss() -> u64 { 21 | os::max_rss() 22 | } 23 | 24 | #[cfg(target_os = "linux")] 25 | mod os { 26 | use super::super::file_to_string; 27 | use super::super::ProbeError; 28 | use super::super::Result; 29 | use std::mem; 30 | use std::path::Path; 31 | 32 | #[inline] 33 | pub fn current_rss() -> Result { 34 | read_and_get_current_rss(&Path::new("/proc/self/statm")) 35 | } 36 | 37 | #[inline] 38 | pub fn current_rss_of(pid: libc::pid_t) -> Result { 39 | read_and_get_current_rss(&Path::new(&format!("/proc/{}/statm", pid))) 40 | } 41 | 42 | #[inline] 43 | pub fn read_and_get_current_rss(path: &Path) -> Result { 44 | let raw_data = file_to_string(path)?; 45 | let segments: Vec<&str> = raw_data.split_whitespace().collect(); 46 | 47 | if segments.len() < 2 { 48 | return Err(ProbeError::UnexpectedContent( 49 | "Incorrect number of segments".to_owned(), 50 | )); 51 | } 52 | 53 | let pages: u64 = segments[1] 54 | .parse() 55 | .map_err(|_| ProbeError::UnexpectedContent("Could not parse segment".to_owned()))?; 56 | 57 | // Value is in pages, needs to be multiplied by the page size to get a value in KB. We ask 58 | // the OS for this information using sysconf. 59 | let pagesize = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as u64 / 1024; 60 | 61 | Ok(pages * pagesize) 62 | } 63 | 64 | #[inline] 65 | pub fn max_rss() -> u64 { 66 | let mut rusage = mem::MaybeUninit::::uninit(); 67 | unsafe { 68 | libc::getrusage(libc::RUSAGE_SELF, rusage.as_mut_ptr()); 69 | rusage.assume_init() 70 | } 71 | .ru_maxrss as u64 72 | } 73 | } 74 | 75 | #[cfg(test)] 76 | #[cfg(target_os = "linux")] 77 | mod tests { 78 | use super::super::ProbeError; 79 | use std::path::Path; 80 | 81 | #[test] 82 | fn test_current_rss() { 83 | assert!(super::current_rss().is_ok()); 84 | // See if it's a sort of sane value, between 1 and 250 mb 85 | assert!(super::current_rss().unwrap() > 1_000); 86 | assert!(super::current_rss().unwrap() < 250_000); 87 | } 88 | 89 | #[test] 90 | fn test_read_and_get_current_rss() { 91 | let path = Path::new("fixtures/linux/process_memory/proc_self_statm"); 92 | let value = super::os::read_and_get_current_rss(&path).unwrap(); 93 | assert_eq!(4552, value); 94 | } 95 | 96 | #[test] 97 | fn test_read_and_get_current_rss_wrong_path() { 98 | let path = Path::new("/nonsense"); 99 | match super::os::read_and_get_current_rss(&path) { 100 | Err(ProbeError::IO(_, _)) => (), 101 | r => panic!("Unexpected result: {:?}", r), 102 | } 103 | } 104 | 105 | #[test] 106 | fn test_read_and_get_current_rss_incomplete() { 107 | let path = Path::new("fixtures/linux/process_memory/proc_self_statm_incomplete"); 108 | match super::os::read_and_get_current_rss(&path) { 109 | Err(ProbeError::UnexpectedContent(_)) => (), 110 | r => panic!("Unexpected result: {:?}", r), 111 | } 112 | } 113 | 114 | #[test] 115 | fn test_read_and_get_current_rss_garbage() { 116 | let path = Path::new("fixtures/linux/process_memory/proc_self_statm_garbage"); 117 | match super::os::read_and_get_current_rss(&path) { 118 | Err(ProbeError::UnexpectedContent(_)) => (), 119 | r => panic!("Unexpected result: {:?}", r), 120 | } 121 | } 122 | 123 | #[test] 124 | fn test_current_rss_of() { 125 | let pid = unsafe { libc::getpid() }; 126 | assert!(super::current_rss_of(pid).is_ok()); 127 | // See if it's a sort of sane value, between 1 and 250 mb 128 | assert!(super::current_rss_of(pid).unwrap() > 1_000); 129 | assert!(super::current_rss_of(pid).unwrap() < 250_000); 130 | } 131 | 132 | #[test] 133 | fn test_current_rss_of_invalid_pid() { 134 | assert!(super::current_rss_of(0).is_err()); 135 | } 136 | 137 | #[test] 138 | fn test_max_rss() { 139 | // See if it's a sort of sane value, between 1 and 700 mb 140 | assert!(super::max_rss() > 1_000); 141 | } 142 | } 143 | --------------------------------------------------------------------------------