Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
MCOfficer committed Apr 1, 2020
0 parents commit a16b21b
Show file tree
Hide file tree
Showing 4 changed files with 309 additions and 0 deletions.
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Generated by Cargo
# will have compiled files and executables
/target/

# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock

# These are backup files generated by rustfmt
**/*.rs.bk
22 changes: 22 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "container-stats"
version = "0.1.0"
authors = ["MCOfficer <[email protected]>"]
description = "A small tool to analyze RAM usage of large amounts of docker containers."
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
proc-maps = "0.1.6"
dockworker = {version="0.0.15", default_features=false, features=[]}
itertools = "0.9.0"
bytesize = "1.0.0"
tabled = "0.1.0"
structopt = "0.3"
fancy-regex = "0.3.3"
procfs = "0.7.8"

[profile.release]
opt-level = "z"
lto = true
94 changes: 94 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# container-stats

A small tool to analyze RAM usage of large amounts of docker containers.

```
container-stats 0.1.0
MCOfficer <[email protected]>
A small tool to analyze RAM usage of large amounts of docker containers.
USAGE:
container-stats [FLAGS] [OPTIONS]
FLAGS:
--group-by-prefix Group containers by prefix
--group-by-suffix Group containers by suffix
-h, --help Prints help information
-s, --sort Sorts containers by memory used
--top Use docker top. Not supported on windows & significantly slower, but correctly detects
multiple processes per container
-t, --total Prints total memory used by containers
-V, --version Prints version information
OPTIONS:
-d, --delimiter <delimiter> Delimiter for grouping [default: -]
-m, --memory-backend <memory-backend> The way the used memory is calculated. Options are: "procmaps" (cross-
platform), "rss" and "vsz" (both linux) [default: procmaps]
-r, --regex <regex> Filters container names by a regular expression [default: .*]
```

## Building

```
$ cargo build [--release]
```

## Samples

List all containers:
```
$ sudo ./container-stats
+----------+--------+----------------------------------------------------------+------------------------------------------------------------------+
| memory | pid | name | id |
+----------+--------+----------------------------------------------------------+------------------------------------------------------------------+
| 4.4 GB | 149498 | /sample-container-1 | 2b13f54b6493cb396ae5122a343053a84a2af5aab1c79eb2f5a58b6a9bb234ca |
+----------+--------+----------------------------------------------------------+------------------------------------------------------------------+
| 4.8 GB | 60271 | /some-slightly-longer-sample-container | 3dd1f8261c053f9ee5908033bd8798a60120e8e0423c74d83bd8dceda22bd157 |
+----------+--------+----------------------------------------------------------+------------------------------------------------------------------+
| 3.5 GB | 28350 | /sample-container-2 | 245c06ad91274c40a7f4e0090d5f74112b74818a4d237b5485962f142e74ac1c |
+----------+--------+----------------------------------------------------------+------------------------------------------------------------------+
| 3.9 GB | 73631 | /some-pretty-damn-long-sample-container-name | 61f2cb9dc16d047f42152dde98c00e5a9f9e2a8ba305adfbf38252af3586e07a |
+----------+--------+----------------------------------------------------------+------------------------------------------------------------------+
| 17.2 GB | 195327 | /sample-container-three | 1b05272735bf083e1b148169130f620505dbc2fcff3c4fc6f8f46e49a4efa676 |
+----------+--------+----------------------------------------------------------+------------------------------------------------------------------+
```

Show the RSS usage of all containers, combined
```
$ sudo ./container-stats -m rss -t
Total: 114.3 GB (114328526848 B)
```
(To learn about the difference between RSS and VSZ, see https://stackoverflow.com/questions/7880784)


The total memory usage of all containers starting with `/sample-`:
```
$ sudo ./container-stats -t -r "^/sample-"
Total: 106.4 GB (106439839744 B)
```

All containers, grouped by their prefixes, sorted by memory usage:
```
$ sudo ./container-stats --group-by-prefix -s
+----------+------------+-------------+
| memory | containers | fix |
+----------+------------+-------------+
| 239.3 GB | 18 | /fatfix |
+----------+------------+-------------+
| 133.1 GB | 10 | /prefix1 |
+----------+------------+-------------+
| 106.4 GB | 24 | /prefix2 |
+----------+------------+-------------+
| 88.3 GB | 7 | /fix |
+----------+------------+-------------+
| 35.8 GB | 10 | /boring |
+----------+------------+-------------+
| 26.1 GB | 6 | /foofix |
+----------+------------+-------------+
| 14.7 GB | 4 | /barfix |
+----------+------------+-------------+
| 11.3 GB | 3 | /prefix4 |
+----------+------------+-------------+
| 127.9 MB | 1 | /tiny |
+----------+------------+-------------+
```
183 changes: 183 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
use bytesize::ByteSize;
use dockworker::container::{Container, ContainerFilters};
use dockworker::Docker;
use itertools::{fold, join};
use proc_maps::{get_process_maps, Pid};
use procfs::process::Process;
use fancy_regex::Regex;
use structopt::StructOpt;
use tabled::{table, Tabled};

#[derive(Tabled)]
struct ContainerStats {
memory: ByteSize,
name: String,
id: String,
}

#[derive(Tabled)]
struct ContainerGroup {
memory: ByteSize,
containers: i32,
fix: String,
}

#[derive(StructOpt, Debug)]
#[structopt(name = "container-stats", author, about)]
struct Opt {
/// Prints total memory used by containers
#[structopt(short, long)]
total: bool,

/// Sorts containers by memory used
#[structopt(short, long)]
sort: bool,

/// Group containers by prefix
#[structopt(long)]
group_by_prefix: bool,

/// Group containers by suffix
#[structopt(long)]
group_by_suffix: bool,

/// Delimiter for grouping
#[structopt(long, short, default_value = "-")]
delimiter: char,

/// Use docker top. Not supported on windows & significantly slower, but correctly detects multiple processes per container
#[structopt(long)]
top: bool,

/// Filters container names by a regular expression
#[structopt(long, short, default_value = ".*")]
regex: String,

/// The way the used memory is calculated. Options are: "procmaps" (cross-platform), "rss" and "vsz" (both linux).
#[structopt(long, short, default_value = "procmaps")]
memory_backend: String,
}

fn main() {
let opt = Opt::from_args();
if !["procmaps".to_owned(), "rss".to_owned(), "vsz".to_owned()].contains(&opt.memory_backend) {
println!("Error: unsupported memory backend");
return;
}

let docker = Docker::connect_with_defaults().unwrap();
match docker.list_containers(None, None, None, ContainerFilters::new()) {
Ok(result) => handle_containers(&opt, docker, result),
Err(e) => println!("Error connecting to docker daemon: {}", e),
};

fn handle_containers(opt: &Opt, docker: Docker, containers: Vec<Container>) {
let mut all_stats = gather_stats(opt, docker, containers);

if !opt.regex.eq(".*") {
all_stats = filter(all_stats, &opt.regex);
}

if opt.total {
let total = fold(&all_stats, 0, |i, stats| i + stats.memory.as_u64());
println!("Total: {} ({} B)", ByteSize::b(total), total);
return;
}

if opt.group_by_prefix || opt.group_by_suffix {
let mut grouped_stats = Vec::<ContainerGroup>::new();
for stat in &all_stats {
let mut split_name = stat.name.split(opt.delimiter);
let fix = match opt.group_by_prefix {
true => split_name.nth(0).unwrap_or(&stat.name).to_string(),
false => split_name.last().unwrap_or(&stat.name).to_string(),
};

let mut found = false;
for group in &mut grouped_stats {
if group.fix.eq(&fix) {
group.memory = group.memory + stat.memory;
group.containers += 1;
found = true;
}
}

if !found {
grouped_stats.push(ContainerGroup {
memory: stat.memory,
containers: 1,
fix: String::from(&fix),
});
}
}

if opt.sort {
grouped_stats.sort_by(|a, b| b.memory.cmp(&a.memory));
}
println!("{}", table(&grouped_stats));
return;
}

if opt.sort {
all_stats.sort_by(|a, b| b.memory.cmp(&a.memory));
}
println!("{}", table(&all_stats));
}

fn gather_stats(opt: &Opt, docker: Docker, containers: Vec<Container>) -> Vec<ContainerStats> {
let mut all_stats = Vec::new();
for container in containers {
let mut memory = 0;
let mut pids = Vec::<i64>::new();

if opt.top {
let processes = docker.processes(&container.Id).unwrap();
for p in processes {
pids.push(p.pid.parse::<i64>().unwrap());
}
}
else {
pids.push(docker.container_info(&container.Id).unwrap().State.Pid);
}

for pid in pids {
memory += get_process_memory_bytes(opt, pid) as u64;
}

all_stats.push(ContainerStats {
id: container.Id,
name: join(container.Names, ", "),
memory: ByteSize::b(memory),
})
}
all_stats
}

fn get_process_memory_bytes(opt: &Opt, pid: i64) -> usize {
let mut memory = 0;

if opt.memory_backend.eq("procmaps") {
let maps = get_process_maps(pid as Pid).unwrap();
for map in maps {
memory += map.size();
}
} else {
let p = Process::new(pid as i32).expect("Failed to access process");
let stat = p.stat().expect("Failed to get process stat");
memory = match opt.memory_backend.as_ref() {
"rss" => stat.rss_bytes() as usize,
"vsz" => stat.vsize as usize,
&_ => panic!("Got unknown memory backend"),
}
}
memory
}

fn filter(stats: Vec<ContainerStats>, pattern: &str) -> Vec<ContainerStats> {
let re = Regex::new(pattern).expect("Failed to create Regex pattern!");
stats
.into_iter()
.filter(|s| re.is_match(&s.name).unwrap())
.collect()
}
}

0 comments on commit a16b21b

Please sign in to comment.