commit 69cb2a4fc80968aa32350cd615677f9e4d97aa87 Author: Ben Charlton Date: Wed Mar 30 19:04:35 2011 +0100 initial commit diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/COPYING @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3afaa56 --- /dev/null +++ b/README.md @@ -0,0 +1,100 @@ += Statgraph = + +== Introduction == + +Statgraph is a simple tool for graphing usage statistic from a number of +unix hosts. + +Statgraph makes use of the statgrab tool from +http://www.i-scream.org/libstatgrab/ and has been tested on Linux, +Solaris and FreeBSD. In theory, any platform supported by libstatgrab +should work. + + +== Usage == +To make use of statgraph, you'll need perl and RRDtool, along with the +perl bindings for RRDtool. On debian, these are included in the +'librrds-perl' package. + +To collect statistics from a host, it will need the statgrab tool +installed from libstatgrab. On a debian/ubuntu system, you can simply +'apt-get install statgrab'. + +You'll need to decide how to collect statistics - either via a direct +TCP connection to the target host, or by executing a command of your +choice. This does mean you can make use of ssh with an ssh key if you +want to keep open ports to a minimum. + +=== Direct TCP === + +If you're collecting via TCP, the easiest way to set things up is to run +statgrab from inetd. + +In /etc/services, add: + + statgrab 27001/tcp + +and in /etc/inetd.conf: + + statgrab stream tcp nowait root /usr/sbin/tcpd /usr/bin/statgrab + +You don't have to run it as root, but there are some statistics that it +doesn't collect as an unprivileged user. The risk is relatively low as +statgrab doesn't accept any input, and will simply print the statistics +and exit, but there is always a risk with exposing a service. As always, +you should seriously consider firewalling access to this port to trusted +hosts only. + +=== Running a command === + +This has a bit more overhead, but does mean minimal changes to the +server you're connecting to. Any command that generates statgrab output +is fine. The simplest option is something like: + + ssh -i ssh_key user@hostname /usr/bin/statgrab + +=== Configuration File === + +The configuration for statgraph is statgraph.conf - this should be +fairly self-explanatory, and a few examples are provided in +statgraph.conf.example + +== Running == + +To check it's all working, run ./statgraph.pl manually. If that looks +good, add to cron and run once per minute. It will email you if a +connection to a host fails though, so you might want to redirect output +to a log file. I don't, since it's useful to know if a host is broken :) + +To generate graphs, run the ./mkgraph.pl script. I run this every 10 +minutes from cron, and it generates static html and .png images in +whatever you've configured the graphs to live. By default this is the +'graphs' directory inside the statgraph directory. + +This directory can be shared by a webserver and contains no dynamic code +whatsoever. + +== Known Issues == + + * Statgraph is insanely spammy if a host is down, unless you redirect + output + + * Mounts with : in the name (remote NFS mounts for example) don't show + up correctly. + + * There's no sanity checking for return values, since they could + massively vary. When I wrote statgraph the idea of 128 core machines + was unimaginable, but we're there now, so I'm reluctant to hard code + any values in. Occasionally a wonky value will make the graph scale a + bit silly. There's a tool called 'rrdtrim' that can fix these. On an + installation monitoring about 40 hosts, I probably have to fix 2 a + year. + + * Running it from cron can be a bit braindead sometime, and the + timeouts could be more effective - occasionally processes do get + wedged if the child does, but it's rare. + +== License == + +Statgraph is released under the GPLv2 license. See the COPYING file for +details. diff --git a/StatGraph.pm b/StatGraph.pm new file mode 100755 index 0000000..50a84a6 --- /dev/null +++ b/StatGraph.pm @@ -0,0 +1,781 @@ +use RRDs; +use strict; + +# Takes array ref of statgrab output. +# Returns hash structure +sub sgparse ($) { + my $text = shift; + my %tree; + foreach (@$text) { + chomp; + m/^([^=]*) = (.*)$/ or die "bad line in statgrab output"; + my @parts = split /\./, $1; + if ($#parts == 2) { + $tree{$parts[0]}{$parts[1]}{$parts[2]} = $2; + } else { + $tree{$parts[0]}{$parts[1]} = $2; + } + } + return %tree; +} + +sub confparse ($) { + my $text = shift; + my %tree; + foreach (@$text) { + chomp; + next if (m/^#/); + next if (m/^$/); + m/^([^=]*) = (.*)$/ or die "bad line in config $_"; + my @parts = split /\./, $1; + if ($#parts == 2) { + $tree{$parts[0]}{$parts[1]}{$parts[2]} = $2; + } else { + $tree{$parts[0]}{$parts[1]} = $2; + } + } + return %tree; +} + +## Connect to remote host and retrieve statgrab results +sub get_net_results ($$$) { + use IO::Socket; + my ($remote_host, $remote_port, $cache) = @_; + my ($line, $response, $socket, $flag, @res); + + $socket = IO::Socket::INET->new(PeerAddr => $remote_host, + PeerPort => $remote_port, + Proto => "tcp", + Type => SOCK_STREAM) + or die "Couldn't connect to $remote_host:$remote_port\n"; + + #while($line = <$socket>){ + # push @res, $line; + #} + @res = (<$socket>); + + my $results = join('', @res); + if ($results ne "") { + open CACHE, ">$cache"; + print CACHE $results; + close CACHE; + } else { + die "No result from $remote_host"; + } + return sgparse(\@res); +} + +## Run command on local host to retrieve statgrab results +## Can be used to get local stats, or call ssh, or similar... +sub get_exec_results ($$) { + my $command = shift; + my $cache = shift; + open STATGRAB, "$command|" or warn "$command failed: $!"; + my @res = (); + + my $results = join('', @res); + if ($results ne "") { + open CACHE, ">$cache"; + print CACHE $results; + close CACHE; + } else { + die "No result from $command"; + } + + return sgparse(\@res); + +} + +## Create RRDs where relevant +sub create_rrd ($$$$) { + my $rrdlocation = shift; + my $type = shift; + my $host = shift; + my $devname = shift; + + use RRDs; + + print " Creating RRD: $host $type $devname\n"; + + if ($type eq 'cpu') { + RRDs::create ("$rrdlocation$host.cpu.rrd", + "--step", "60", + "DS:idle:COUNTER:120:U:U", + "DS:iowait:COUNTER:120:U:U", + "DS:kernel:COUNTER:120:U:U", + "DS:nice:COUNTER:120:U:U", + "DS:swap:COUNTER:120:U:U", + "DS:total:COUNTER:120:U:U", + "DS:user:COUNTER:120:U:U", + "RRA:AVERAGE:0.5:1:2160", # 1.5 days + "RRA:AVERAGE:0.5:15:1008", # 1.5 weeks + "RRA:AVERAGE:0.5:60:1008", # 6 weeks + "RRA:AVERAGE:0.5:720:1460", # 2 years + "RRA:MAX:0.5:1:2160", # 1.5 days + "RRA:MAX:0.5:15:1008", # 1.5 weeks + "RRA:MAX:0.5:60:1008", # 6 weeks + "RRA:MAX:0.5:720:1460"); # 2 years + + } elsif ($type eq 'load') { + RRDs::create ("$rrdlocation$host.load.rrd", + "--step", "60", + "DS:min1:GAUGE:120:U:U", + "DS:min5:GAUGE:120:U:U", + "DS:min15:GAUGE:120:U:U", + "RRA:AVERAGE:0.5:1:2160", # 1.5 days + "RRA:AVERAGE:0.5:15:1008", # 1.5 weeks + "RRA:AVERAGE:0.5:60:1008", # 6 weeks + "RRA:AVERAGE:0.5:720:1460", # 2 years + "RRA:MAX:0.5:1:2160", # 1.5 days + "RRA:MAX:0.5:15:1008", # 1.5 weeks + "RRA:MAX:0.5:60:1008", # 6 weeks + "RRA:MAX:0.5:720:1460"); # 2 years + + } elsif ($type eq 'mem') { + RRDs::create ("$rrdlocation$host.mem.rrd", + "--step", "60", + "DS:cache:GAUGE:120:U:U", + "DS:free:GAUGE:120:U:U", + "DS:total:GAUGE:120:U:U", + "DS:used:GAUGE:120:U:U", + "RRA:AVERAGE:0.5:1:2160", # 1.5 days + "RRA:AVERAGE:0.5:15:1008", # 1.5 weeks + "RRA:AVERAGE:0.5:60:1008", # 6 weeks + "RRA:AVERAGE:0.5:720:1460", # 2 years + "RRA:MAX:0.5:1:2160", # 1.5 days + "RRA:MAX:0.5:15:1008", # 1.5 weeks + "RRA:MAX:0.5:60:1008", # 6 weeks + "RRA:MAX:0.5:720:1460"); # 2 years + + } elsif ($type eq 'page') { + RRDs::create ("$rrdlocation$host.page.rrd", + "--step", "60", + "DS:in:COUNTER:120:U:U", + "DS:out:COUNTER:120:U:U", + "RRA:AVERAGE:0.5:1:2160", # 1.5 days + "RRA:AVERAGE:0.5:15:1008", # 1.5 weeks + "RRA:AVERAGE:0.5:60:1008", # 6 weeks + "RRA:AVERAGE:0.5:720:1460", # 2 years + "RRA:MAX:0.5:1:2160", # 1.5 days + "RRA:MAX:0.5:15:1008", # 1.5 weeks + "RRA:MAX:0.5:60:1008", # 6 weeks + "RRA:MAX:0.5:720:1460"); # 2 years + + } elsif ($type eq 'proc') { + RRDs::create ("$rrdlocation$host.proc.rrd", + "--step", "60", + "DS:running:GAUGE:120:U:U", + "DS:sleeping:GAUGE:120:U:U", + "DS:stopped:GAUGE:120:U:U", + "DS:total:GAUGE:120:U:U", + "DS:zombie:GAUGE:120:U:U", + "RRA:AVERAGE:0.5:1:2160", # 1.5 days + "RRA:AVERAGE:0.5:15:1008", # 1.5 weeks + "RRA:AVERAGE:0.5:60:1008", # 6 weeks + "RRA:AVERAGE:0.5:720:1460", # 2 years + "RRA:MAX:0.5:1:2160", # 1.5 days + "RRA:MAX:0.5:15:1008", # 1.5 weeks + "RRA:MAX:0.5:60:1008", # 6 weeks + "RRA:MAX:0.5:720:1460"); # 2 years + + } elsif ($type eq 'user') { + RRDs::create ("$rrdlocation$host.user.rrd", + "--step", "60", + "DS:num:GAUGE:120:U:U", + "RRA:AVERAGE:0.5:1:2160", # 1.5 days + "RRA:AVERAGE:0.5:15:1008", # 1.5 weeks + "RRA:AVERAGE:0.5:60:1008", # 6 weeks + "RRA:AVERAGE:0.5:720:1460", # 2 years + "RRA:MAX:0.5:1:2160", # 1.5 days + "RRA:MAX:0.5:15:1008", # 1.5 weeks + "RRA:MAX:0.5:60:1008", # 6 weeks + "RRA:MAX:0.5:720:1460"); # 2 years + + } elsif ($type eq 'swap') { + RRDs::create ("$rrdlocation$host.swap.rrd", + "--step", "60", + "DS:free:GAUGE:120:U:U", + "DS:total:GAUGE:120:U:U", + "DS:used:GAUGE:120:U:U", + "RRA:AVERAGE:0.5:1:2160", # 1.5 days + "RRA:AVERAGE:0.5:15:1008", # 1.5 weeks + "RRA:AVERAGE:0.5:60:1008", # 6 weeks + "RRA:AVERAGE:0.5:720:1460", # 2 years + "RRA:MAX:0.5:1:2160", # 1.5 days + "RRA:MAX:0.5:15:1008", # 1.5 weeks + "RRA:MAX:0.5:60:1008", # 6 weeks + "RRA:MAX:0.5:720:1460"); # 2 years + + + } elsif ($type eq 'disk') { + RRDs::create ("$rrdlocation$host.disk.$devname.rrd", + "--step", "60", + "DS:read_bytes:COUNTER:120:U:U", + "DS:write_bytes:COUNTER:120:U:U", + "RRA:AVERAGE:0.5:1:2160", # 1.5 days + "RRA:AVERAGE:0.5:15:1008", # 1.5 weeks + "RRA:AVERAGE:0.5:60:1008", # 6 weeks + "RRA:AVERAGE:0.5:720:1460", # 2 years + "RRA:MAX:0.5:1:2160", # 1.5 days + "RRA:MAX:0.5:15:1008", # 1.5 weeks + "RRA:MAX:0.5:60:1008", # 6 weeks + "RRA:MAX:0.5:720:1460"); # 2 years + + } elsif ($type eq 'net') { + RRDs::create ("$rrdlocation$host.net.$devname.rrd", + "--step", "60", + "DS:rx:COUNTER:120:U:U", + "DS:tx:COUNTER:120:U:U", + "DS:ipackets:COUNTER:120:U:U", + "DS:opackets:COUNTER:120:U:U", + "DS:ierrors:COUNTER:120:U:U", + "DS:oerrors:COUNTER:120:U:U", + "RRA:AVERAGE:0.5:1:2160", # 1.5 days + "RRA:AVERAGE:0.5:15:1008", # 1.5 weeks + "RRA:AVERAGE:0.5:60:1008", # 6 weeks + "RRA:AVERAGE:0.5:720:1460", # 2 years + "RRA:MAX:0.5:1:2160", # 1.5 days + "RRA:MAX:0.5:15:1008", # 1.5 weeks + "RRA:MAX:0.5:60:1008", # 6 weeks + "RRA:MAX:0.5:720:1460"); # 2 years + } elsif ($type eq 'fs') { + RRDs::create ("$rrdlocation$host.fs.$devname.rrd", + "--step", "60", + "DS:used:GAUGE:120:U:U", + "DS:size:GAUGE:120:U:U", + "DS:used_inodes:GAUGE:120:U:U", + "DS:total_inodes:GAUGE:120:U:U", + "RRA:AVERAGE:0.5:1:2160", # 1.5 days + "RRA:AVERAGE:0.5:15:1008", # 1.5 weeks + "RRA:AVERAGE:0.5:60:1008", # 6 weeks + "RRA:AVERAGE:0.5:720:1460", # 2 years + "RRA:MAX:0.5:1:2160", # 1.5 days + "RRA:MAX:0.5:15:1008", # 1.5 weeks + "RRA:MAX:0.5:60:1008", # 6 weeks + "RRA:MAX:0.5:720:1460"); # 2 years + } +} + +sub update_rrd ($$) { + my $rrd = shift; + my $data = shift; + #print "$rrd, $data\n"; + RRDs::update($rrd, $data); + my $ERR=RRDs::error; + print "ERROR while updating $rrd: $ERR\n" if $ERR; +} + +sub create_graph ($$$$$$$$) { + my $rrdlocation = shift; + my $location = shift; + my $type = shift; + my $host = shift; + my $devname = shift; + my $offsets = shift; + my $friendly = shift; + my $colours = shift; + my %colours = %$colours; + + # Check colours + $colours{stack1} = '#FF0000' unless $colours{stack1}; + $colours{stack2} = '#FFFF00' unless $colours{stack2}; + $colours{stack3} = '#00FFFF' unless $colours{stack3}; + $colours{stack4} = '#00FF00' unless $colours{stack4}; + $colours{stack5} = '#0000FF' unless $colours{stack5}; + + $colours{load1} = '#CECFFF' unless $colours{load1}; + $colours{load5} = '#7375FF' unless $colours{load5}; + $colours{load15} = '#0000FF' unless $colours{load15}; + + $colours{area} = '#CECFFF' unless $colours{area}; + $colours{line} = '#0000FF' unless $colours{line}; + + $colours{in} = '#00FF00' unless $colours{in}; + $colours{out} = '#0000FF' unless $colours{out}; + + foreach my $offset (@$offsets) { + + if ($type eq 'cpu') { + create_graph_cpu($rrdlocation, $location, $host, $offset, \%colours); + } elsif ($type eq 'load') { + create_graph_load($rrdlocation, $location, $host, $offset, \%colours); + } elsif ($type eq 'mem') { + create_graph_mem($rrdlocation, $location, $host, $offset, \%colours); + } elsif ($type eq 'page') { + create_graph_page($rrdlocation, $location, $host, $offset, \%colours); + } elsif ($type eq 'proc') { + create_graph_proc($rrdlocation, $location, $host, $offset, \%colours); + } elsif ($type eq 'user') { + create_graph_user($rrdlocation, $location, $host, $offset, \%colours); + } elsif ($type eq 'swap') { + create_graph_swap($rrdlocation, $location, $host, $offset, \%colours); + } elsif ($type eq 'disk') { + create_graph_disk($rrdlocation, $location, $host, $devname, $offset, \%colours); + } elsif ($type eq 'net') { + create_graph_net($rrdlocation, $location, $host, $devname, $offset, \%colours); + } elsif ($type eq 'fs') { + create_graph_fs($rrdlocation, $location, $host, $devname, $offset, $friendly, \%colours); + } + + } +} + +sub create_graph_cpu ($$$$$) { + my $rrdlocation = shift; + my $location = shift; + my $host = shift; + my $offset = shift; + my $colours = shift; + my %colours = %{$colours}; + my $type = "cpu"; + + RRDs::graph ("$location$host.$type.$offset.png", + "--start", "-$offset", + "-l", "0", + "-a", "PNG", + "-t", "CPU usage for $host", + "--vertical-label", "% cpu used", + "DEF:idle=$rrdlocation$host.$type.rrd:idle:AVERAGE", + "DEF:iowait=$rrdlocation$host.$type.rrd:iowait:AVERAGE", + "DEF:kernel=$rrdlocation$host.$type.rrd:kernel:AVERAGE", + "DEF:nice=$rrdlocation$host.$type.rrd:nice:AVERAGE", + "DEF:swap=$rrdlocation$host.$type.rrd:swap:AVERAGE", + "DEF:user=$rrdlocation$host.$type.rrd:user:AVERAGE", + "AREA:kernel$colours{stack1}:kernel cpu", + "GPRINT:kernel:LAST:Current\\: \%8.2lf %s", + "GPRINT:kernel:AVERAGE:Average\\: \%8.2lf %s", + "GPRINT:kernel:MAX:Max\\: \%8.2lf %s\\n", + "STACK:swap$colours{stack2}:swap cpu ", + "GPRINT:swap:LAST:Current\\: \%8.2lf %s", + "GPRINT:swap:AVERAGE:Average\\: \%8.2lf %s", + "GPRINT:swap:MAX:Max\\: \%8.2lf %s\\n", + "STACK:iowait$colours{stack3}:iowait cpu", + "GPRINT:iowait:LAST:Current\\: \%8.2lf %s", + "GPRINT:iowait:AVERAGE:Average\\: \%8.2lf %s", + "GPRINT:iowait:MAX:Max\\: \%8.2lf %s\\n", + "STACK:nice$colours{stack4}:nice cpu ", + "GPRINT:nice:LAST:Current\\: \%8.2lf %s", + "GPRINT:nice:AVERAGE:Average\\: \%8.2lf %s", + "GPRINT:nice:MAX:Max\\: \%8.2lf %s\\n", + "STACK:user$colours{stack5}:user cpu ", + "GPRINT:user:LAST:Current\\: \%8.2lf %s", + "GPRINT:user:AVERAGE:Average\\: \%8.2lf %s", + "GPRINT:user:MAX:Max\\: \%8.2lf %s\\n"); + my $ERR=RRDs::error; + die "ERROR : $ERR\n" if $ERR; +} + +sub create_graph_load ($$$$$) { + my $rrdlocation = shift; + my $location = shift; + my $host = shift; + my $offset = shift; + my $colours = shift; + my %colours = %{$colours}; + my $type = "load"; + + RRDs::graph ("$location$host.$type.$offset.png", + "--start", "-$offset", + "-l", "0", + "-u", "1", + "-a", "PNG", + "-t", "load averages for $host", + "--units-exponent", "1", + "--vertical-label", "processes in run queue", + "DEF:load1=$rrdlocation$host.$type.rrd:min1:AVERAGE", + "DEF:load5=$rrdlocation$host.$type.rrd:min5:AVERAGE", + "DEF:load15=$rrdlocation$host.$type.rrd:min15:AVERAGE", + "LINE2:load1$colours{load1}:1 minute load ", + "GPRINT:load1:LAST:Current\\: \%8.2lf %s", + "GPRINT:load1:AVERAGE:Average\\: \%8.2lf %s", + "GPRINT:load1:MAX:Max\\: \%8.2lf %s\\n", + "LINE2:load5$colours{load5}:5 minute load ", + "GPRINT:load5:LAST:Current\\: \%8.2lf %s", + "GPRINT:load5:AVERAGE:Average\\: \%8.2lf %s", + "GPRINT:load5:MAX:Max\\: \%8.2lf %s\\n", + "LINE2:load15$colours{load15}:15 minute load", + "GPRINT:load15:LAST:Current\\: \%8.2lf %s", + "GPRINT:load15:AVERAGE:Average\\: \%8.2lf %s", + "GPRINT:load15:MAX:Max\\: \%8.2lf %s\\n"); + my $ERR=RRDs::error; + die "ERROR : $ERR\n" if $ERR; +} + +sub create_graph_mem ($$$$$) { + my $rrdlocation = shift; + my $location = shift; + my $host = shift; + my $offset = shift; + my $colours = shift; + my %colours = %{$colours}; + my $type = "mem"; + + RRDs::graph ("$location$host.$type.$offset.png", + "--start", "-$offset", + "-l", "0", + "-a", "PNG", + "-u", "100", + "-t", "memory usage for $host", + "--base", "1024", + "--vertical-label", "% memory used", + "DEF:free=$rrdlocation$host.$type.rrd:free:AVERAGE", + "DEF:cache=$rrdlocation$host.$type.rrd:cache:AVERAGE", + "DEF:used=$rrdlocation$host.$type.rrd:used:AVERAGE", + "DEF:total=$rrdlocation$host.$type.rrd:total:AVERAGE", + "CDEF:peruse=total,free,total,LT,free,0,IF,-,total,/,100,*", + "CDEF:percacuse=cache,total,LT,cache,0,IF,total,/,100,*", + "AREA:peruse$colours{area}:Used ", + "GPRINT:peruse:LAST:Current\\: \%8.2lf %s", + "GPRINT:peruse:AVERAGE:Average\\: \%8.2lf %s", + "GPRINT:peruse:MAX:Max\\: \%8.2lf %s\\n", + "LINE2:percacuse$colours{line}:Cache", + "GPRINT:percacuse:LAST:Current\\: \%8.2lf %s", + "GPRINT:percacuse:AVERAGE:Average\\: \%8.2lf %s", + "GPRINT:percacuse:MAX:Max\\: \%8.2lf %s\\n", + "GPRINT:total:LAST:Current total memory\\: \%.2lf %sb\\c"); + + my $ERR=RRDs::error; + die "ERROR : $ERR\n" if $ERR; +} + +sub create_graph_proc ($$$$$) { + my $rrdlocation = shift; + my $location = shift; + my $host = shift; + my $offset = shift; + my $colours = shift; + my %colours = %{$colours}; + my $type = "proc"; + + RRDs::graph ("$location$host.$type.$offset.png", + "--start", "-$offset", + "-l", "0",, + "-a", "PNG", + "-t", "processes on $host", + "--units-exponent", "1", + "--vertical-label", "num of processes", + "DEF:running=$rrdlocation$host.$type.rrd:running:AVERAGE", + "DEF:sleeping=$rrdlocation$host.$type.rrd:sleeping:AVERAGE", + "DEF:stopped=$rrdlocation$host.$type.rrd:stopped:AVERAGE", + "DEF:zombie=$rrdlocation$host.$type.rrd:zombie:AVERAGE", + "AREA:stopped$colours{stack1}:stopped processes ", + "GPRINT:stopped:LAST:Current\\: \%8.2lf %s", + "GPRINT:stopped:AVERAGE:Average\\: \%8.2lf %s", + "GPRINT:stopped:MAX:Max\\: \%8.2lf %s\\n", + "STACK:zombie$colours{stack2}:zombie processes ", + "GPRINT:zombie:LAST:Current\\: \%8.2lf %s", + "GPRINT:zombie:AVERAGE:Average\\: \%8.2lf %s", + "GPRINT:zombie:MAX:Max\\: \%8.2lf %s\\n", + "STACK:sleeping$colours{stack3}:sleeping processes", + "GPRINT:sleeping:LAST:Current\\: \%8.2lf %s", + "GPRINT:sleeping:AVERAGE:Average\\: \%8.2lf %s", + "GPRINT:sleeping:MAX:Max\\: \%8.2lf %s\\n", + "STACK:running$colours{stack4}:running processes ", + "GPRINT:running:LAST:Current\\: \%8.2lf %s", + "GPRINT:running:AVERAGE:Average\\: \%8.2lf %s", + "GPRINT:running:MAX:Max\\: \%8.2lf %s\\n"); + + my $ERR=RRDs::error; + die "ERROR : $ERR\n" if $ERR; +} + +sub create_graph_page ($$$$$) { + my $rrdlocation = shift; + my $location = shift; + my $host = shift; + my $offset = shift; + my $colours = shift; + my %colours = %{$colours}; + my $type = "page"; + + RRDs::graph ("$location$host.$type.$offset.png", + "--start", "-$offset", + "-l", "0",, + "-a", "PNG", + "-t", "paging activity on $host", + "--units-exponent", "1", + "--vertical-label", "pages/second", + "DEF:in=$rrdlocation$host.$type.rrd:in:AVERAGE", + "DEF:out=$rrdlocation$host.$type.rrd:out:AVERAGE", + "AREA:in$colours{in}:pages in ", + "GPRINT:in:LAST:Current\\: \%8.2lf %s", + "GPRINT:in:AVERAGE:Average\\: \%8.2lf %s", + "GPRINT:in:MAX:Max\\: \%8.2lf %s\\n", + "LINE2:out$colours{out}:pages out", + "GPRINT:out:LAST:Current\\: \%8.2lf %s", + "GPRINT:out:AVERAGE:Average\\: \%8.2lf %s", + "GPRINT:out:MAX:Max\\: \%8.2lf %s\\n"); + + my $ERR=RRDs::error; + die "ERROR : $ERR\n" if $ERR; +} + +sub create_graph_user ($$$$$) { + my $rrdlocation = shift; + my $location = shift; + my $host = shift; + my $offset = shift; + my $colours = shift; + my %colours = %{$colours}; + my $type = "user"; + + RRDs::graph ("$location$host.$type.$offset.png", + "--start", "-$offset", + "-l", "0", + "-u", "1", + "-a", "PNG", + "-t", "users on $host", + "--units-exponent", "1", + "--vertical-label", "users logged in", + "DEF:num=$rrdlocation$host.$type.rrd:num:AVERAGE", + "LINE2:num$colours{line}:Logged in users", + "GPRINT:num:LAST:Current\\: \%8.2lf %s", + "GPRINT:num:AVERAGE:Average\\: \%8.2lf %s", + "GPRINT:num:MAX:Max\\: \%8.2lf %s\\n"); + + my $ERR=RRDs::error; + die "ERROR : $ERR\n" if $ERR; +} + +sub create_graph_swap ($$$$$) { + my $rrdlocation = shift; + my $location = shift; + my $host = shift; + my $offset = shift; + my $colours = shift; + my %colours = %{$colours}; + my $type = "swap"; + + RRDs::graph ("$location$host.$type.$offset.png", + "--start", "-$offset", + "-l", "0", + "-a", "PNG", + "-u", "100", + "-t", "swap use on $host", + "--base", "1024", + "--vertical-label", "% swap used", + "DEF:free=$rrdlocation$host.$type.rrd:free:AVERAGE", + "DEF:used=$rrdlocation$host.$type.rrd:used:AVERAGE", + "DEF:total=$rrdlocation$host.$type.rrd:total:AVERAGE", + "CDEF:peruse=total,free,total,LT,free,0,IF,-,total,/,100,*", + "AREA:peruse$colours{area}:Used", + "GPRINT:peruse:LAST:Current\\: \%8.2lf %s", + "GPRINT:peruse:AVERAGE:Average\\: \%8.2lf %s", + "GPRINT:peruse:MAX:Max\\: \%8.2lf %s\\n", + "GPRINT:total:LAST:Current total swap\\: \%.2lf %sb\\c"); + + my $ERR=RRDs::error; + die "ERROR : $ERR\n" if $ERR; +} + +sub create_graph_disk ($$$$$$) { + my $rrdlocation = shift; + my $location = shift; + my $host = shift; + my $dev = shift; + my $offset = shift; + my $colours = shift; + my %colours = %{$colours}; + my $type = "disk"; + + RRDs::graph ("$location$host.$type.$dev.$offset.png", + "--start", "-$offset", + "-l", "0", + "-a", "PNG", + "-t", "disk io on $host for $dev", + "--base", "1024", + "--vertical-label", "bytes per second", + "DEF:read_bytes=$rrdlocation$host.$type.$dev.rrd:read_bytes:AVERAGE", + "DEF:write_bytes=$rrdlocation$host.$type.$dev.rrd:write_bytes:AVERAGE", + "AREA:read_bytes$colours{in}:read bytes ", + "GPRINT:read_bytes:LAST:Current\\: \%8.2lf %s", + "GPRINT:read_bytes:AVERAGE:Average\\: \%8.2lf %s", + "GPRINT:read_bytes:MAX:Max\\: \%8.2lf %s\\n", + "LINE2:write_bytes$colours{out}:write bytes", + "GPRINT:write_bytes:LAST:Current\\: \%8.2lf %s", + "GPRINT:write_bytes:AVERAGE:Average\\: \%8.2lf %s", + "GPRINT:write_bytes:MAX:Max\\: \%8.2lf %s\\n"); + + my $ERR=RRDs::error; + die "ERROR : $ERR\n" if $ERR; +} + +sub create_graph_net ($$$$$$) { + my $rrdlocation = shift; + my $location = shift; + my $host = shift; + my $dev = shift; + my $offset = shift; + my $colours = shift; + my %colours = %{$colours}; + my $type = "net"; + + RRDs::graph ("$location$host.$type.$dev.$offset.png", + "--start", "-$offset", + "-l", "0", + "-a", "PNG", + "-t", "network io on $host for $dev", + "--base", "1024", + "--vertical-label", "bytes per second", + "DEF:rx=$rrdlocation$host.$type.$dev.rrd:rx:AVERAGE", + "DEF:tx=$rrdlocation$host.$type.$dev.rrd:tx:AVERAGE", + "AREA:rx$colours{in}:recieved bytes ", + "GPRINT:rx:LAST:Current\\: \%8.2lf %s", + "GPRINT:rx:AVERAGE:Average\\: \%8.2lf %s", + "GPRINT:rx:MAX:Max\\: \%8.2lf %s\\n", + "LINE2:tx$colours{out}:transmitted bytes", + "GPRINT:tx:LAST:Current\\: \%8.2lf %s", + "GPRINT:tx:AVERAGE:Average\\: \%8.2lf %s", + "GPRINT:tx:MAX:Max\\: \%8.2lf %s\\n"); + + my $ERR=RRDs::error; + die "ERROR : $ERR\n" if $ERR; +} + +sub create_graph_fs ($$$$$$$) { + my $rrdlocation = shift; + my $location = shift; + my $host = shift; + my $dev = shift; + my $offset = shift; + my $mountpoint = shift || $dev; + my $colours = shift; + my %colours = %{$colours}; + my $type = "fs"; + $dev =~ s/:/\\:/g; + RRDs::graph ("$location$host.$type.$dev.$offset.png", + "--start", "-$offset", + "-l", "0", + "-a", "PNG", + "-u", "100", + "-t", "disk usage on $host for $mountpoint", + "--base", "1024", + "--vertical-label", "% used", + "DEF:used=$rrdlocation$host.$type.$dev.rrd:used:AVERAGE", + "DEF:size=$rrdlocation$host.$type.$dev.rrd:size:AVERAGE", + "DEF:used_inodes=$rrdlocation$host.$type.$dev.rrd:used_inodes:AVERAGE", + "DEF:total_inodes=$rrdlocation$host.$type.$dev.rrd:total_inodes:AVERAGE", + "CDEF:peruse=used,size,/,100,*", + "CDEF:perinode=used_inodes,total_inodes,/,100,*", + "AREA:peruse$colours{area}:space used ", + "GPRINT:peruse:LAST:Current\\: \%8.2lf %s", + "GPRINT:peruse:AVERAGE:Average\\: \%8.2lf %s", + "GPRINT:peruse:MAX:Max\\: \%8.2lf %s\\n", + "GPRINT:size:LAST:Current total space\\: \%.2lf %sb\\c", + "LINE2:perinode$colours{line}:inodes used", + "GPRINT:perinode:LAST:Current\\: \%8.2lf %s", + "GPRINT:perinode:AVERAGE:Average\\: \%8.2lf %s", + "GPRINT:perinode:MAX:Max\\: \%8.2lf %s\\n", + "GPRINT:total_inodes:LAST:Current total inodes\\: \%.2lf %s\\c"); + + my $ERR=RRDs::error; + die "ERROR : $ERR\n" if $ERR; +} + +sub htmlheader ($) { + my $title = shift; + return " + $title + "; +} + +sub htmlfooter () { + return "

Powered by StatGraph

+ "; +} + + +sub create_page ($$$$$) { + my $location = shift; + my $type = shift; + my $host = shift; + my $devname = shift; + my $offsets = shift; + + # print " Creating graphs: $host $type $devname\n"; + + my %nicenames = ( + 'cpu' => "CPU utilisation for $host", + 'load' => "Load averages for $host", + 'mem' => "Memory usage for $host", + 'page' => "Paging activity for $host", + 'proc' => "Processes for $host", + 'user' => "User activity for $host", + 'swap' => "Swap usage for $host", + 'disk' => "Disk IO for $host on $devname", + 'net' => "Network IO for $host on $devname", + 'fs' => "Filsystem Utilisation for $host on $devname"); + + + my $dev = ""; + $dev = ".$devname" if ($devname ne ""); + + open OUT, ">$location$host-$type$dev.html"; + print OUT htmlheader("$nicenames{$type}"); + print OUT "

$nicenames{$type}

\n"; + print OUT "

Last updated: " . nice_date(); + + foreach my $offset (@$offsets) { + print OUT "

" . nice_time($offset). "


\n"; + } + close OUT; +} + +sub nice_time ($) { + my $seconds = shift; + my $minute = '60'; + my $hour = '3600'; + my $day = 24 * $hour; + my $week = 7 * $day; + my $month = 4 * $week; + my $year = 365 * $day; + + my @stack; + + my $tmp = int($seconds / $year); + if ($tmp > 0) { + push @stack, "$tmp years"; + $seconds = $seconds % $year; + } + + $tmp = int($seconds / $month); + if ($tmp > 0) { + push @stack, "$tmp months"; + $seconds = $seconds % $month; + } + + $tmp = int($seconds / $week); + if ($tmp > 0) { + push @stack, "$tmp weeks"; + $seconds = $seconds % $week; + } + + $tmp = int($seconds / $day); + if ($tmp > 0) { + push @stack, "$tmp days"; + $seconds = $seconds % $day; + } + + $tmp = int($seconds / $hour); + if ($tmp > 0) { + push @stack, "$tmp hours"; + $seconds = $seconds % $hour; + } + + $tmp = int($seconds / $minute); + if ($tmp > 0) { + push @stack, "$tmp minutes"; + $seconds = $seconds % $minute; + } + + push (@stack, "$seconds seconds") if ($seconds > 0); + + return join (", ", @stack); +} + +sub nice_date () { + my ($seconds, $minutes, $hours, $day_of_month, $month, $year, $wday, $yday, $isdst) = localtime(time); + return sprintf("%02d:%02d:%02d-%04d/%02d/%02d\n", $hours, $minutes, $seconds, $year+1900, $month+1, $day_of_month); +} + +1; diff --git a/graphs/sg-p-small.png b/graphs/sg-p-small.png new file mode 100755 index 0000000..28fc987 Binary files /dev/null and b/graphs/sg-p-small.png differ diff --git a/graphs/sg-p.png b/graphs/sg-p.png new file mode 100755 index 0000000..71b9c13 Binary files /dev/null and b/graphs/sg-p.png differ diff --git a/graphs/sg-p.psp b/graphs/sg-p.psp new file mode 100755 index 0000000..571adca Binary files /dev/null and b/graphs/sg-p.psp differ diff --git a/graphs/sg-small.png b/graphs/sg-small.png new file mode 100755 index 0000000..5623044 Binary files /dev/null and b/graphs/sg-small.png differ diff --git a/graphs/sg.png b/graphs/sg.png new file mode 100755 index 0000000..87eb5b3 Binary files /dev/null and b/graphs/sg.png differ diff --git a/graphs/sg.psp b/graphs/sg.psp new file mode 100755 index 0000000..b576996 Binary files /dev/null and b/graphs/sg.psp differ diff --git a/mkconf.pl b/mkconf.pl new file mode 100755 index 0000000..4581507 --- /dev/null +++ b/mkconf.pl @@ -0,0 +1,12 @@ +#!/usr/bin/perl +$name = $ARGV[0]; +$dnsdomainname = `dnsdomainname`; +chomp($dnsdomainname); + +print "hosts.$name.displayname = $name.$dnsdomainname +hosts.$name.method = connect +hosts.$name.hostname = $name +hosts.$name.port = 27001 +hosts.$name.comment = $name + +" diff --git a/mkgraph.pl b/mkgraph.pl new file mode 100755 index 0000000..baf48dc --- /dev/null +++ b/mkgraph.pl @@ -0,0 +1,124 @@ +#!/usr/bin/perl + +use strict; +use StatGraph; +use File::Copy; + +## TODO: configure with getopt +my $config = "statgraph.conf"; + +## Get configuration +open CONFIG, $config || die "Cannot open $config: $!"; +my @CONFIG = (); +close CONFIG; + +my %CONFIG = confparse(\@CONFIG); +my %HOSTS = %{$CONFIG{hosts}}; +%CONFIG = %{$CONFIG{config}}; + +## Set default configuration options if they've not been specified +my $defaultport = $CONFIG{defaultport} || 27001; +my $offsets = $CONFIG{offsets} || '10800 86400 604800 2419200 31536000'; +my @offsets = split(/\s+/, $offsets); + +my $rrdlocation = $CONFIG{rrdlocation} || "rrd/"; +my $graphlocation = $CONFIG{graphlocation} || "graphs/"; + + +open INDEX, ">$graphlocation/index.html.tmp"; +print INDEX htmlheader("StatGraph results"); +print INDEX "

StatGraph results

"; +print INDEX "

Last updated: " . nice_date; +print INDEX "

    "; + + +## Main loop - run once for each host +foreach my $host (sort keys %HOSTS) { + print "+ $HOSTS{$host}{displayname}\n"; + print INDEX "
  • $host"; + print INDEX " - $HOSTS{$host}{comment}
  • "; + + my %ignore; + + foreach (split(/\s+/, $HOSTS{$host}{ignore})) { + $ignore{$_} = 1; + } + + open CACHE, "$rrdlocation$host.txt" || warn "$host has no cache\n"; + my @res = (); + close CACHE; + my %results = sgparse(\@res); + + + ## Simple check that we've got reasonable statgrab data + unless ($results{const}{0} eq '0') { + warn "Bad statgrab results for $host"; + next; + } + + open SUMM, ">$graphlocation$host.html"; + print SUMM htmlheader("$host summary"); + my %colours = %{$CONFIG{colour}}; + + print SUMM "

    $host summary for last " . nice_time($offsets[0]). "

    "; + print SUMM "

    $HOSTS{$host}{comment}

    "; + print SUMM "

    Last updated: " . nice_date; + + my %nicenames = ( + 'cpu' => "CPU utilisation for $host", + 'load' => "Load averages for $host", + 'mem' => "Memory usage for $host", + 'page' => "Paging activity for $host", + 'proc' => "Processes for $host", + 'user' => "User activity for $host", + 'swap' => "Swap usage for $host"); + + ## Generate graphs + foreach ('cpu', 'load', 'mem', 'page', 'proc', 'user', 'swap') { + if (-e "$rrdlocation$host.$_.rrd") { + create_graph($rrdlocation, $graphlocation, $_, $host, '', \@offsets, '', \%colours); + create_page($graphlocation, $_, $host, '', \@offsets); + print SUMM "

    $nicenames{$_}


    \n"; + } + } + + ## net device RRDs + foreach my $dev (sort keys %{ $results{net} }) { + unless (defined $ignore{"net.$dev"}) { + if (-e "$rrdlocation$host.net.$dev.rrd") { + create_graph($rrdlocation, $graphlocation, 'net', $host, $dev, \@offsets, '', \%colours); + create_page($graphlocation, 'net', $host, $dev, \@offsets); + print SUMM "

    Network IO for $host on $dev


    \n"; + } + } + } + + ## disk device RRDs + foreach my $dev (sort keys %{ $results{disk} }) { + unless (defined $ignore{"disk.$dev"}) { + if (-e "$rrdlocation$host.disk.$dev.rrd") { + create_graph($rrdlocation, $graphlocation, 'disk', $host, $dev, \@offsets, '', \%colours); + create_page($graphlocation, 'disk', $host, $dev, \@offsets); + print SUMM "

    Disk IO for $host on $dev


    \n"; + } + } + } + + ## fs device RRDs + foreach my $dev (sort keys %{ $results{fs} }) { + unless (defined $ignore{"fs.$dev"}) { + if (-e "$rrdlocation$host.fs.$dev.rrd") { + create_graph($rrdlocation, $graphlocation, 'fs', $host, $dev, \@offsets, $results{fs}{$dev}{mnt_point}, \%colours); + create_page($graphlocation, 'fs', $host, $dev, \@offsets); + print SUMM "

    Filesystem Utilisation for $host on $dev


    \n"; + } + } + } + print SUMM htmlfooter; + close SUMM; +} +print INDEX "
"; +print INDEX htmlfooter; +close INDEX; + +move("$graphlocation/index.html.tmp", "$graphlocation/index.html"); diff --git a/statgraph.conf.example b/statgraph.conf.example new file mode 100755 index 0000000..4e5202b --- /dev/null +++ b/statgraph.conf.example @@ -0,0 +1,69 @@ +########################### +# Default config options +# +# All these are optional, and override the defaults + +## Number of seconds from "now" to generate graphs +## Default 3h 1d 1w 1m 1y +config.offsets = 10800 86400 604800 2419200 31536000 + +## Location for graphs and rrd files +config.graphlocation = graphs/ +config.rrdlocation = rrd/ + +## Default port for 'connect' method +config.defaultport = 27001 + +## Colours for graphs +# contrasty stack colours, eg cpu. +config.colour.stack1 = #FF0000 +config.colour.stack2 = #FFFF00 +config.colour.stack3 = #00FFFF +config.colour.stack4 = #00FF00 +config.colour.stack5 = #0000FF + +# load colours +config.colour.load1 = #CECFFF +config.colour.load5 = #7375FF +config.colour.load15 = #0000FF + +# colours for line on top of area graphs, like swap/mem +config.colour.area = #CECFFF +config.colour.line = #0000FF + +# in refers to rx, bytes_read, etc... +config.colour.in = #00FF00 +config.colour.out = #0000FF + +########################### +# Hosts section +# +# REQUIRED: hosts.NAME.displayname +# REQUIRED: hosts.NAME.method +# OPTIONAL: hosts.NAME.comment +# OPTIONAL: hosts.NAME.ignore +# +# if using method = exec +# hosts.NAME.execcommand is REQUIRED +# +# if using method = connect +# hosts.NAME.hostname is REQUIRED +# hosts.NAME.port is OPTIONAL + +hosts.kitten.displayname = kitten.example.com +hosts.kitten.method = connect +hosts.kitten.hostname = kitten.example.com +hosts.kitten.port = 27001 +hosts.kitten.comment = development machine +hosts.kitten.ignore = net.dummy0 net.lo + +hosts.nero.displayname = nero.example.com +hosts.nero.method = exec +hosts.nero.execcommand = ssh nero.example.com /usr/bin/statgrab +hosts.nero.comment = some random server + +hosts.localhost.displayname = localhost.example.com +hosts.localhost.method = exec +hosts.localhost.execcommand = /usr/bin/statgrab +hosts.localhost.comment = This host + diff --git a/statgraph.pl b/statgraph.pl new file mode 100755 index 0000000..f61b4ec --- /dev/null +++ b/statgraph.pl @@ -0,0 +1,201 @@ +#!/usr/bin/perl + +# statgraph: A simple host resource graphing tool +# Copyright (C) 2004-2011 Ben Charlton +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; see the file COPYING. If not, write to the Free +# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301 USA, or see http://www.gnu.org + +use strict; +use StatGraph; + +## TODO: configure with getopt +my $config = "statgraph.conf"; + +## Get configuration +open CONFIG, $config || die "Cannot open $config: $!"; +my @CONFIG = (); +close CONFIG; + +my %CONFIG = confparse(\@CONFIG); +my %HOSTS = %{$CONFIG{hosts}}; +%CONFIG = %{$CONFIG{config}}; + +## Set default configuration options if they've not been specified +my $defaultport = $CONFIG{defaultport} || 27001; +my $rrdlocation = $CONFIG{rrdlocation} || "rrd/"; + + +my $pid = $$; +my $parent = 0; +my @kids = (); +my $host; + +## Main loop - run once for each host +FORK: foreach my $currhost (keys %HOSTS) { + + my $newpid = fork(); + if ( not defined $newpid ) { + # if return value of fork() is undef, something went wrong + die "fork didn't work: $!\n"; + } + elsif ( $newpid == 0 ) { + # if return value is 0, this is the child process + $parent = $pid; # which has a parent called $pid + $pid = $$; # and which will have a process ID of its very own + @kids = (); # the child doesn't want this baggage from the parent + $host = $currhost; + last FORK; # and we don't want the child making babies either + } + else { + # the parent process is returned the PID of the newborn by fork() + push @kids, $newpid; + } + +} + +if ( $parent ) { + # if I have a parent, i.e. if I'm the child process + my %results; + my %ignore; + + foreach (split(/\s+/, $HOSTS{$host}{ignore})) { + $ignore{$_} = 1; + } + + ## exec method + if ($HOSTS{$host}{method} eq 'exec') { + unless (defined $HOSTS{$host}{execcommand}) { + die "execcommand not specified for $host, skipping..."; + } + %results = get_exec_results($HOSTS{$host}{execcommand}, "$rrdlocation$host.txt"); + + ## Network socket connection method + } elsif ($HOSTS{$host}{method} eq 'connect') { + unless (defined $HOSTS{$host}{hostname}) { + die "hostname not specified for $host, skipping..."; + } + my $port = $HOSTS{$host}{port} || $defaultport; + %results = get_net_results($HOSTS{$host}{hostname}, $port, "$rrdlocation$host.txt"); + + ## Unknown method + } else { + die "Unknown method specified for $host, skipping..."; + } + + ## Simple check that we've got reasonable statgrab data + unless ($results{const}{0} eq '0') { + die "Bad statgrab results"; + } else { + print "got result for $host\n"; + } + + ## Update RRDs + foreach ('cpu', 'load', 'mem', 'page', 'proc', 'user', 'swap') { + unless (-e "$rrdlocation$host.$_.rrd") { + create_rrd($rrdlocation, $_, $host, ''); + } + } + update_rrd("$rrdlocation$host.cpu.rrd", sprintf("N:%s:%s:%s:%s:%s:%s:%s", + $results{cpu}{idle}, + $results{cpu}{iowait}, + $results{cpu}{kernel}, + $results{cpu}{nice}, + $results{cpu}{swap}, + $results{cpu}{total}, + $results{cpu}{user})); + + update_rrd("$rrdlocation$host.load.rrd", sprintf("N:%s:%s:%s", + $results{load}{min1}, + $results{load}{min5}, + $results{load}{min15})); + + update_rrd("$rrdlocation$host.mem.rrd", sprintf("N:%s:%s:%s:%s", + $results{mem}{cache}, + $results{mem}{free}, + $results{mem}{total}, + $results{mem}{used})); + + update_rrd("$rrdlocation$host.page.rrd", sprintf("N:%s:%s", + $results{page}{in}, + $results{page}{out})); + + update_rrd("$rrdlocation$host.proc.rrd", sprintf("N:%s:%s:%s:%s:%s", + $results{proc}{running}, + $results{proc}{sleeping}, + $results{proc}{stopped}, + $results{proc}{total}, + $results{proc}{zombie})); + + update_rrd("$rrdlocation$host.user.rrd", sprintf("N:%s", + $results{user}{num})); + + update_rrd("$rrdlocation$host.swap.rrd", sprintf("N:%s:%s:%s", + $results{swap}{free}, + $results{swap}{total}, + $results{swap}{used})); + + ## net device RRDs + foreach my $dev (keys %{ $results{net} }) { + unless (defined $ignore{"net.$dev"}) { + unless (-e "$rrdlocation$host.net.$dev.rrd") { + create_rrd($rrdlocation, 'net', $host, $dev); + } + update_rrd("$rrdlocation$host.net.$dev.rrd", sprintf("N:%s:%s:%s:%s:%s:%s", + $results{net}{$dev}{rx} || 0, + $results{net}{$dev}{tx} || 0, + $results{net}{$dev}{ipackets} || 0, + $results{net}{$dev}{opackets} || 0, + $results{net}{$dev}{ierrors} || 0, + $results{net}{$dev}{oerrors} || 0)); + } + } + + ## disk device RRDs + foreach my $dev (keys %{ $results{disk} }) { + unless (defined $ignore{"disk.$dev"}) { + unless (-e "$rrdlocation$host.disk.$dev.rrd") { + create_rrd($rrdlocation, 'disk', $host, $dev); + } + update_rrd("$rrdlocation$host.disk.$dev.rrd", sprintf("N:%s:%s", + $results{disk}{$dev}{read_bytes}, + $results{disk}{$dev}{write_bytes})); + } + } + + ## fs device RRDs + foreach my $dev (keys %{ $results{fs} }) { + unless (defined $ignore{"fs.$dev"}) { + unless (-e "$rrdlocation$host.fs.$dev.rrd") { + create_rrd($rrdlocation, 'fs', $host, $dev); + } + update_rrd("$rrdlocation$host.fs.$dev.rrd", sprintf("N:%s:%s:%s:%s", + $results{fs}{$dev}{used}, + $results{fs}{$dev}{size}, + $results{fs}{$dev}{used_inodes}, + $results{fs}{$dev}{total_inodes})); + } + } +} + +else { + # parent process needs to preside over the death of its kids + while ( my $kid = shift @kids ) { + my $reaped = waitpid( $kid, 0 ); + unless ( $reaped == $kid ) { + warn "Something's up: $?\n"; + } + } +}