├── LICENSE ├── README.rst ├── linux-stats-web ├── menu-images │ ├── button1a.gif │ ├── button4.gif │ └── button4a.gif ├── stats_linux.html ├── stats_linux_1440.html ├── stats_linux_240.html ├── stats_linux_60.html └── style.css ├── screenshots ├── net_bps_in_rrd_240.png ├── stats_linux_240.png └── stats_linux_index.png └── stats2rrd.py /LICENSE: -------------------------------------------------------------------------------- 1 | This file is part of linux-stats-dashboard. 2 | 3 | Copyright (c) 2010-2011 Corey Goldberg (http://goldb.org) 4 | 5 | License :: OSI Approved :: MIT License: 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | http://www.opensource.org/licenses/mit-license 18 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | linux-stats-dashboard 3 | ===================== 4 | 5 | local system monitoring dashboard for linux 6 | 7 | * © 2010-2011 `Corey Goldberg `_ 8 | * https://github.com/cgoldberg/linux-stats-dashboard 9 | 10 | ---- 11 | Info 12 | ---- 13 | 14 | * collects and graphs basic operating system stats 15 | * stores data in RRD (round-robin database) 16 | * generates .png images of plots/stats 17 | * run this script at regular intervals with a task/job scheduler 18 | * requires: python 2.x, rrdtool, linux 2.6+ 19 | 20 | ------------ 21 | Instructions 22 | ------------ 23 | 24 | * configure the script settings: 25 | 26 | * **NET_INTERFACE**: network device namde 27 | * **DISK**: storage device name 28 | * **INTERVAL**: collection interval in secs (how often the script is run) 29 | * **GRAPH_MINS**: timespans for graph/png files 30 | * **GRAPH_DIR**: output directory for png images 31 | 32 | * perhaps under '/var/www/' 33 | * make sure directory is writable 34 | 35 | * **STORAGE_DIR**: output directory for rrd database files 36 | 37 | * make sure directory is writable 38 | 39 | * make the script executable:: 40 | 41 | $ chmod +x stats2rrd.py 42 | 43 | * add an entry to your crontab (crontab -e) so cron will run it. 44 | 45 | * example crontab entry using a 60 sec (1 min) interval:: 46 | 47 | */1 * * * * /home/corey/linux-stats-dashboard/stats2rrd.py 48 | 49 | --------------- 50 | Stats Collected 51 | --------------- 52 | 53 | * **cpu_percent**: processor utilization 54 | * **mem_used**: physical memory usage 55 | * **net_bps_in**: network throughput (bps in) 56 | * **net_bps_out**: network throughput (bps out) 57 | * **load_avg**: system load average (1 min) 58 | * **disk_busy_percent**: disk busy doing i/o 59 | 60 | ------------ 61 | Dependencies 62 | ------------ 63 | 64 | * linux 2.6+ 65 | * python 2.x 66 | * rrdtool 67 | 68 | ------- 69 | License 70 | ------- 71 | 72 | * This is Free Open Source Software. 73 | * OSI Approved :: `MIT License `_ 74 | 75 | ----------- 76 | Screenshots 77 | ----------- 78 | 79 | .. image:: https://raw.github.com/cgoldberg/linux-stats-dashboard/master/screenshots/stats_linux_240.png 80 | .. image:: https://raw.github.com/cgoldberg/linux-stats-dashboard/master/screenshots/stats_linux_index.png 81 | .. image:: https://raw.github.com/cgoldberg/linux-stats-dashboard/master/screenshots/net_bps_in_rrd_240.png 82 | -------------------------------------------------------------------------------- /linux-stats-web/menu-images/button1a.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgoldberg/linux-stats-dashboard/8c26174aae8c2347f0d197d54e5ae587c1625877/linux-stats-web/menu-images/button1a.gif -------------------------------------------------------------------------------- /linux-stats-web/menu-images/button4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgoldberg/linux-stats-dashboard/8c26174aae8c2347f0d197d54e5ae587c1625877/linux-stats-web/menu-images/button4.gif -------------------------------------------------------------------------------- /linux-stats-web/menu-images/button4a.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgoldberg/linux-stats-dashboard/8c26174aae8c2347f0d197d54e5ae587c1625877/linux-stats-web/menu-images/button4a.gif -------------------------------------------------------------------------------- /linux-stats-web/stats_linux.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Linux OS Stats 4 | 5 | 6 | 7 |
8 | 14 |
15 | 16 |

Linux OS Stats

17 |

(select a time span)

18 | 19 |
20 | 21 |

Available Stats:

22 | 23 | 31 | 32 |
33 | 34 | -------------------------------------------------------------------------------- /linux-stats-web/stats_linux_1440.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Linux OS Stats 4 | 5 | 6 | 7 | 8 | 9 |
10 | 16 |
17 | 18 |

Linux OS Stats

19 | 20 |

Last 24 Hours (1440 mins)

21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |

CPU (Utilization)

Memory (Used Physical)

32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |

Network (Throughput bps In)

Network (Throughput bps Out)

43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |

System Load Average (1 min)

Disk (Busy)

54 | 55 | 56 | -------------------------------------------------------------------------------- /linux-stats-web/stats_linux_240.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Linux OS Stats 4 | 5 | 6 | 7 | 8 | 9 |
10 | 16 |
17 | 18 |

Linux OS Stats

19 | 20 |

Last 4 Hours (240 mins)

21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |

CPU (Utilization)

Memory (Used Physical)

32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |

Network (Throughput bps In)

Network (Throughput bps Out)

43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |

System Load Average (1 min)

Disk (Busy)

54 | 55 | 56 | -------------------------------------------------------------------------------- /linux-stats-web/stats_linux_60.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Linux OS Stats 4 | 5 | 6 | 7 | 8 | 9 |
10 | 16 |
17 | 18 |

Linux OS Stats

19 | 20 |

Last Hour (60 mins)

21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |

CPU (Utilization)

Memory (Used Physical)

32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |

Network (Throughput bps In)

Network (Throughput bps Out)

43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |

System Load Average (1 min)

Disk (Busy)

54 | 55 | 56 | -------------------------------------------------------------------------------- /linux-stats-web/style.css: -------------------------------------------------------------------------------- 1 | 2 | .menu {padding:0 0 0 32px; margin:0; list-style:none; height:40px; background:#fff url(menu-images/button1a.gif) repeat-x; position:relative; font-family:verdana, sans-serif; } 3 | .menu li.top {display:block; float:left; position:relative;} 4 | .menu li a.top_link {display:block; float:left; height:40px; line-height:33px; color:#bbb; text-decoration:none; font-size:11px; font-weight:bold; padding:0 0 0 12px; cursor:pointer;} 5 | .menu li a.top_link span {float:left; font-weight:bold; display:block; padding:0 24px 0 12px; height:40px;} 6 | .menu li a.top_link span.down {float:left; display:block; padding:0 24px 0 12px; height:40px; background:url(menu-images/down.gif) no-repeat right top;} 7 | .menu li a.top_link:hover {color:#000; background: url(menu-images/button4.gif) no-repeat;} 8 | .menu li a.top_link:hover span {background:url(menu-images/button4.gif) no-repeat right top;} 9 | .menu li a.top_link:hover span.down {background:url(menu-images/button4a.gif) no-repeat right top;} 10 | .menu li:hover > a.top_link {color:#000; background: url(menu-images/button4.gif) no-repeat;} 11 | .menu li:hover > a.top_link span {background:url(menu-images/button4.gif) no-repeat right top;} 12 | .menu li:hover > a.top_link span.down {background:url(menu-images/button4a.gif) no-repeat right top;} 13 | .menu table {border-collapse:collapse; width:0; height:0; position:absolute; top:0; left:0;} 14 | .menu a:hover {visibility:visible;} 15 | .menu li:hover {position:relative; z-index:200;} 16 | 17 | body { 18 | background-color: #FFFFFF; 19 | color: #000000; 20 | font-family: Ubuntu, Verdana, Helvetica, sans-serif; 21 | font-size: 12px; 22 | margin: 0 0 0 0; 23 | } 24 | p { 25 | margin-left: 5px; 26 | } 27 | table { 28 | margin-left: 5px; 29 | } 30 | h1 { 31 | font-size: 16px; 32 | margin-left: 5px; 33 | } 34 | h2 { 35 | font-size: 14px; 36 | background: #C0C0C0; 37 | margin-left: 5px; 38 | margin-right: 5px; 39 | padding-left: 5px; 40 | } 41 | h3 { 42 | font-size: 12px; 43 | background: #EEEEEE; 44 | margin-bottom: 0; 45 | padding-left: 5px; 46 | } 47 | h4 { 48 | font-size: 12px; 49 | padding-left: 5px; 50 | margin-bottom: 0; 51 | } 52 | 53 | -------------------------------------------------------------------------------- /screenshots/net_bps_in_rrd_240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgoldberg/linux-stats-dashboard/8c26174aae8c2347f0d197d54e5ae587c1625877/screenshots/net_bps_in_rrd_240.png -------------------------------------------------------------------------------- /screenshots/stats_linux_240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgoldberg/linux-stats-dashboard/8c26174aae8c2347f0d197d54e5ae587c1625877/screenshots/stats_linux_240.png -------------------------------------------------------------------------------- /screenshots/stats_linux_index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgoldberg/linux-stats-dashboard/8c26174aae8c2347f0d197d54e5ae587c1625877/screenshots/stats_linux_index.png -------------------------------------------------------------------------------- /stats2rrd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) 2010-2011 Corey Goldberg (http://goldb.org) 4 | # 5 | # License: MIT (http://www.opensource.org/licenses/mit-license) 6 | # 7 | # This file is part of linux-stats-dashboard. 8 | # 9 | 10 | 11 | """stats2rrd.py - collect and graph linux operating system stats""" 12 | 13 | 14 | import os.path 15 | import shlex 16 | import socket 17 | import subprocess 18 | import time 19 | 20 | 21 | 22 | # Config Settings 23 | NET_INTERFACE = 'eth0' 24 | DISK = 'sda' 25 | INTERVAL = 60 # 1 min 26 | GRAPH_MINS = (60, 240, 1440) # 1hour, 4hours, 1day 27 | GRAPH_DIR = './linux-stats-web/' 28 | STORAGE_DIR = './' 29 | 30 | 31 | 32 | def main(): 33 | cpu_pct = cpu_util(5) 34 | 35 | mem_used, mem_total = mem_stats() 36 | 37 | rx_bits, tx_bits = net_stats(NET_INTERFACE) 38 | 39 | load_avg = load_avg_1min() 40 | 41 | disk_pct = disk_busy(DISK, 5) 42 | 43 | localhost_name = socket.gethostname() 44 | 45 | # store values in rrd and update graphs 46 | rrd_ops('cpu_percent', cpu_pct, 'GAUGE', 'FF0000', localhost_name, 1000, upper_limit=100) 47 | rrd_ops('mem_used', mem_used, 'GAUGE', '00FF00', localhost_name, 1024, upper_limit=mem_total) 48 | rrd_ops('net_bps_in', rx_bits, 'DERIVE', '6666FF', localhost_name, 1000) 49 | rrd_ops('net_bps_out', tx_bits, 'DERIVE', '000099', localhost_name, 1000) 50 | rrd_ops('load_avg', load_avg, 'GAUGE', 'FF9933', localhost_name, 1000) 51 | rrd_ops('disk_busy_percent', disk_pct, 'GAUGE', '663366', localhost_name, 1000, upper_limit=100) 52 | 53 | 54 | def rrd_ops(stat, value, ds_type, color, title, base, upper_limit=None): 55 | rrd_name = '%s.rrd' % stat 56 | rrd = RRD(rrd_name, stat) 57 | rrd.upper_limit = upper_limit 58 | rrd.base = base 59 | rrd.graph_title = title 60 | rrd.graph_color = color 61 | rrd.graph_dir = GRAPH_DIR 62 | rrd.storage_dir = STORAGE_DIR 63 | if not os.path.exists(os.path.join(STORAGE_DIR, rrd_name)): 64 | rrd.create(INTERVAL, ds_type) 65 | rrd.update(value) 66 | for mins in GRAPH_MINS: 67 | rrd.graph(mins) 68 | print time.strftime('%Y/%m/%d %H:%M:%S', time.localtime()), stat, value 69 | 70 | 71 | def net_stats(interface): 72 | for line in open('/proc/net/dev'): 73 | if interface in line: 74 | data = line.split('%s:' % interface)[1].split() 75 | rx_bits, tx_bits = (int(data[0]) * 8, int(data[8]) * 8) 76 | return (rx_bits, tx_bits) 77 | 78 | 79 | def mem_stats(): 80 | with open('/proc/meminfo') as f: 81 | for line in f: 82 | if line.startswith('MemTotal:'): 83 | mem_total = int(line.split()[1]) * 1024 84 | if line.startswith('MemFree:'): 85 | mem_used = mem_total - (int(line.split()[1]) * 1024) 86 | return mem_used, mem_total 87 | 88 | 89 | def cpu_util(sample_duration=1): 90 | with open('/proc/stat') as f1: 91 | with open('/proc/stat') as f2: 92 | line1 = f1.readline() 93 | time.sleep(sample_duration) 94 | line2 = f2.readline() 95 | deltas = [int(b) - int(a) for a, b in zip(line1.split()[1:], line2.split()[1:])] 96 | idle_delta = deltas[3] 97 | total = sum(deltas) 98 | util_pct = 100 * (float(total - idle_delta) / total) 99 | return util_pct 100 | 101 | 102 | def disk_busy(device, sample_duration=1): 103 | with open('/proc/diskstats') as f1: 104 | with open('/proc/diskstats') as f2: 105 | content1 = f1.read() 106 | time.sleep(sample_duration) 107 | content2 = f2.read() 108 | sep = '%s ' % device 109 | io_ms1 = '0' 110 | io_ms2 = '0' 111 | for line in content1.splitlines(): 112 | if sep in line: 113 | io_ms1 = line.strip().split(sep)[1].split()[9] 114 | break 115 | for line in content2.splitlines(): 116 | if sep in line: 117 | io_ms2 = line.strip().split(sep)[1].split()[9] 118 | break 119 | delta = int(io_ms2) - int(io_ms1) 120 | total = sample_duration * 1000 121 | busy_pct = 100 - (100 * (float(total - delta) / total)) 122 | return busy_pct 123 | 124 | 125 | def load_avg_1min(): 126 | with open('/proc/loadavg') as f: 127 | line = f.readline() 128 | load_avg = float(line.split()[0]) # 1 minute load average 129 | return load_avg 130 | 131 | 132 | 133 | class RRD(object): 134 | def __init__(self, rrd_name, stat): 135 | self.stat = stat 136 | self.rrd_name = rrd_name 137 | self.rrd_exe = 'rrdtool' 138 | self.upper_limit = None 139 | self.base = 1000 # for traffic measurement, 1 kb/s is 1000 b/s. for sizing, 1 kb is 1024 bytes. 140 | self.graph_title = '' 141 | self.graph_dir = './' 142 | self.storage_dir = './' 143 | self.graph_color = 'FF6666' 144 | self.graph_width = 480 145 | self.graph_height = 160 146 | 147 | 148 | def create(self, interval, ds_type='GAUGE'): 149 | interval = str(interval) 150 | interval_mins = float(interval) / 60 151 | heartbeat = str(int(interval) * 2) 152 | ds_string = ' DS:ds:%s:%s:0:U' % (ds_type, heartbeat) 153 | cmd_create = ''.join((self.rrd_exe, 154 | ' create ', os.path.join(self.storage_dir, self.rrd_name), ' --step ', interval, ds_string, 155 | ' RRA:AVERAGE:0.5:1:', str(int(4000 / interval_mins)), 156 | ' RRA:AVERAGE:0.5:', str(int(30 / interval_mins)), ':800', 157 | ' RRA:AVERAGE:0.5:', str(int(120 / interval_mins)), ':800', 158 | ' RRA:AVERAGE:0.5:', str(int(1440 / interval_mins)), ':800')) 159 | p = subprocess.Popen(shlex.split(cmd_create), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False) 160 | cmd_output = p.communicate()[0].rstrip() 161 | if len(cmd_output) > 0: 162 | raise RRDError('unable to create RRD: %s' % cmd_output) 163 | 164 | 165 | def update(self, value): 166 | cmd_update = '%s update %s N:%s' % (self.rrd_exe, os.path.join(self.storage_dir, self.rrd_name), value) 167 | p = subprocess.Popen(shlex.split(cmd_update), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False) 168 | cmd_output = p.communicate()[0].rstrip() 169 | if len(cmd_output) > 0: 170 | raise RRDError('unable to update RRD: %s' % cmd_output) 171 | 172 | 173 | def graph(self, mins): 174 | start_time = 'now-%s' % (mins * 60) 175 | output_filename = '%s_%i.png' % (self.rrd_name, mins) 176 | end_time = 'now' 177 | cur_date = time.strftime('%m/%d/%Y %H\:%M\:%S', time.localtime()) 178 | cmd = [self.rrd_exe, 'graph', os.path.join(self.graph_dir, output_filename)] 179 | cmd.append('COMMENT:\\s') 180 | cmd.append('COMMENT:%s ' % cur_date) 181 | cmd.append('DEF:ds=%s:ds:AVERAGE' % os.path.join(self.storage_dir, self.rrd_name)) 182 | cmd.append('AREA:ds#%s:%s ' % (self.graph_color, self.stat)) 183 | cmd.append('VDEF:dslast=ds,LAST') 184 | cmd.append('VDEF:dsavg=ds,AVERAGE') 185 | cmd.append('VDEF:dsmin=ds,MINIMUM') 186 | cmd.append('VDEF:dsmax=ds,MAXIMUM') 187 | cmd.append('COMMENT:\\s') 188 | cmd.append('COMMENT:\\s') 189 | cmd.append('COMMENT:\\s') 190 | cmd.append('COMMENT:\\s') 191 | cmd.append('GPRINT:dslast:last %.1lf%S ') 192 | cmd.append('GPRINT:dsavg:avg %.1lf%S ') 193 | cmd.append('GPRINT:dsmin:min %.1lf%S ') 194 | cmd.append('GPRINT:dsmax:max %.1lf%S ') 195 | cmd.append('COMMENT:\\s') 196 | cmd.append('COMMENT:\\s') 197 | cmd.append('--title=%s' % self.graph_title) 198 | cmd.append('--vertical-label=%s' % self.stat) 199 | cmd.append('--start=%s' % start_time) 200 | cmd.append('--end=%s' % end_time) 201 | cmd.append('--width=%i' % self.graph_width) 202 | cmd.append('--height=%i' % self.graph_height) 203 | cmd.append('--base=%i' % self.base) 204 | cmd.append('--slope-mode') 205 | if self.upper_limit is not None: 206 | cmd.append('--upper-limit=%i' % self.upper_limit) 207 | cmd.append('--lower-limit=0') 208 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False) 209 | cmd_output = p.communicate()[0].rstrip() 210 | if len(cmd_output) > 10: 211 | raise RRDError('unable to graph RRD: %s' % cmd_output) 212 | 213 | 214 | 215 | class RRDError(Exception): pass 216 | 217 | 218 | 219 | if __name__ == '__main__': 220 | main() 221 | --------------------------------------------------------------------------------