#!/usr/bin/perl -w # # ======================================================================== # sar2rrd.pl # ------------------------------------------------------------------------ # # This script can be used to parse sar command output. # It will create RRDTool archives and graphs. # # You can use command line arguments to select the graphs # and the columns you wish. # # Common bug: lines of input text must end by '\n' not '\r\n' ! # # ------------------------------------------------------------------------ # Author: Jerome Delamarche (jd@maje.biz - jd@trickytools.com) # Version: 2.2 # Date: 25 Sep 2007 # # Changes: # added a new option ("-c") to ignore error with rrdtool updates # option -S (step) can specify an interval smaller than the interval # between the 2 first samples (ex: -S 60 when the first # interval indicates 61s) # # ------------------------------------------------------------------------ # Version: 2.1 # Date: 20 Aug 2007 # # Changes: # Complete rewriting of the parsing loop # Handle format of SunOS sar output # Handle incomplete lines: # timestamp (nothing on the line) # timestamp col1 col2 (other columns missing) # RRD archive now includes the hostname # ------------------------------------------------------------------------ # # On some Solaris, the swpq-sz and %swpocc columns of the runq_sz # report may be empty. values are no longer reported. This will be fixed # in the script, but as a workaround you can specify the graph and # the valid columns using the '-g' option on the command line. # # Note: in case of cross-over, there must be at least 3 measures # before midnight... # # ======================================================================== # use strict; use Getopt::Std; use Time::Local; use Date::Calc qw(Add_Delta_Days); # Parameters overridable via the command line: my $uVerbose = 0; my $uContinueOnErrors = 0; #my $uDateFormat = "MDY"; # format of the date on the 1st output line my $uDateFormat = "DMY"; # format of the date on the 1st output line my $uStartDate = ""; my $uEndDate = ""; my $uStep = ""; my $uSarFile = ""; my $uRRDDir = "./rrd"; my $uImgDir = "./img"; my $uImgWidth = 400; my $uImgHeight = 200; my $uLogarithmic = ""; my $uGraphNameSpec = ""; my $uGraphColSign = ""; my @uGraphColSpec = (); # Global parameters: my $gRRDTool = "/usr/local/rrdtool/bin/rrdtool"; my $gOSName = ""; my $gHostName = ""; my $gStartTime = ""; my $gStartSec = ""; my $gStartDay = ""; my $gStartMonth = ""; my $gStartYear = ""; my $gEndTime = ""; my $gEndSec = ""; my $gEndDay = ""; my $gEndMonth = ""; my $gEndYear = ""; my $gDeltaSec = 0; my $gCuritem = ""; my $gMeasureCount = 0; # Graph parameters: my $gLineWidth = 1; my @gColors = ( "FF0000", "0000FF", "00FFFF", "FF00FF", "FFFF00", "00FF00", "000000", "C0C0C0", "FF8C00", "8FBC8F" ); my $gGraphStartSec = 0; my $gGraphEndSec = 0; # "multiple" graphs are stats like Block Device or CPU Usage that indicate one line per item #14:46:56 DEV tps rd_sec/s wr_sec/s #14:47:01 dev1-0 0,00 0,00 0,00 #14:47:01 dev1-1 0,00 0,00 0,00 # Note about the source names: # '/' must be substituted by a '_' # '%' must be substituted by 'prct_' my %gAllSarStats = ( # Linux indicators: "linux" => { "proc_s" => { "ignore_col" => undef, "title" => "Process per Second", "unit" => "count/s", "multiple" => 0, "keysize" => 1, }, "runq_sz" => { "ignore_col" => undef, "title" => "Queue Size and Load Average", "unit" => "count", "multiple" => 0, "keysize" => 1, }, "tps" => { "ignore_col" => undef, "title" => "I/O Transfer Rate", "unit" => "count/s", "multiple" => 0, "keysize" => 1, }, "pgpgin_s" => { "ignore_col" => undef, "title" => "Paging Statistics", "unit" => "count/s", "multiple" => 0, "keysize" => 1, }, "DEV" => { "ignore_col" => undef, "title" => "Block Device Activity", "unit" => "count/s", "multiple" => 1, "keysize" => 1, }, "INTR" => { "ignore_col" => undef, "title" => "Interrupt Count", "unit" => "count/s", "multiple" => 1, "keysize" => 1, }, "IFACE" => { "ignore_col" => undef, "title" => { "rxpck_s" => "Network Statistics", "rxerr_s" => "Network Failure Statistics", }, "unit" => "count/s", "multiple" => 1, "keysize" => 2, }, "totsck" => { "ignore_col" => undef, "title" => "Socket Statistics", "unit" => "count", "multiple" => 0, "keysize" => 1, }, "kbmemfree" => { "ignore_col" => undef, "title" => "Memory and Swap Utilization", "unit" => "count", "multiple" => 0, "keysize" => 1, }, "frmpg_s" => { "ignore_col" => undef, "title" => "Memory Statistics", "unit" => "count", "multiple" => 0, "keysize" => 1, }, "CPU" => { "ignore_col" => undef, "title" => { "prct_user" => "CPU Utilization", "i000_s" => "Interruption Statistics", }, "unit" => "count", "multiple" => 1, "keysize" => 2, }, "dentunusd" => { "ignore_col" => undef, "title" => "Inode and Files Statistics", "unit" => "count", "multiple" => 0, "keysize" => 1, }, "cswch_s" => { "ignore_col" => undef, "title" => "Context Switches per Second", "unit" => "count", "multiple" => 0, "keysize" => 1, }, "pswpin_s" => { "ignore_col" => undef, "title" => "Swapping Statistics", "unit" => "count", "multiple" => 0, "keysize" => 1, }, # call/s added from version 2.1 "call_s" => { "ignore_col" => undef, "title" => "NFS Client Activity", "unit" => "count", "multiple" => 0, "keysize" => 1, }, # scall/s added from version 2.1 "scall_s" => { "ignore_col" => undef, "title" => "NFS Server Activity", "unit" => "count", "multiple" => 0, "keysize" => 1, }, # scall/s added from version 2.1 "TTY" => { "ignore_col" => undef, "title" => "TTY Device Activity", "unit" => "count", "multiple" => 0, "keysize" => 1, }, }, # SunOS indicators: "sunos" => { "runq_sz" => { "ignore_col" => undef, "title" => "Queue Size and Load Average", "unit" => "count", "multiple" => 0, "keysize" => 1, }, "iget_s" => { "ignore_col" => undef, "title" => "Inode Statistics", "unit" => "count", "multiple" => 0, "keysize" => 1, }, "freemem" => { "ignore_col" => undef, "title" => "Memory Statistics", "unit" => "count", "multiple" => 0, "keysize" => 1, }, "prct_usr" => { "ignore_col" => undef, "title" => "CPU Load", "unit" => "percentage", "multiple" => 0, "keysize" => 1, }, "bread_s" => { "ignore_col" => undef, "title" => "Buffer Activity", "unit" => "count", "multiple" => 0, "keysize" => 1, }, "swpin_s" => { "ignore_col" => undef, "title" => "Swapping Statistics", "unit" => "count", "multiple" => 0, "keysize" => 1, }, "scall_s" => { "ignore_col" => undef, "title" => "System Calls", "unit" => "count", "multiple" => 0, "keysize" => 1, }, "rawch_s" => { "ignore_col" => undef, "title" => "TTY Device Activity", "unit" => "count", "multiple" => 0, "keysize" => 1, }, "proc_sz" => { "ignore_col" => undef, "title" => "Process and Inodes Status", "unit" => "count", "multiple" => 0, "keysize" => 1, }, "msg_s" => { "ignore_col" => undef, "title" => "Message and Semaphore", "unit" => "count", "multiple" => 0, "keysize" => 1, }, "atch_s" => { "ignore_col" => undef, "title" => "Paging Activity", "unit" => "count", "multiple" => 0, "keysize" => 1, }, "pgout_s" => { "ignore_col" => undef, "title" => "Paging Activity", "unit" => "count", "multiple" => 0, "keysize" => 1, }, "sml_mem" => { "ignore_col" => undef, "title" => "Kernel Memory Allocation", "unit" => "count", "multiple" => 0, "keysize" => 1, }, "device" => { "ignore_col" => undef, "title" => "Device Activity", "unit" => "count", "multiple" => 1, "keysize" => 1, }, }, ); my $gSarStats; # --------------------------------------------------------------------- # Command line analysis: # --------------------------------------------------------------------- my %opts = (); getopts("?cd:i:t:f:oH:W:vS:e:s:g:",\%opts); if (exists($opts{'?'})) { Usage(); } if (exists($opts{'c'})) { $uContinueOnErrors = 1; } if (exists($opts{'d'})) { $uRRDDir = $opts{'d'}; } if (exists($opts{'i'})) { $uImgDir = $opts{'i'}; } if (exists($opts{'f'})) { $uSarFile = $opts{'f'}; } if (exists($opts{'t'})) { $uDateFormat = $opts{'t'}; } if (exists($opts{'H'})) { $uImgHeight = $opts{'H'}; } if (exists($opts{'W'})) { $uImgWidth = $opts{'W'}; } if (exists($opts{'o'})) { $uLogarithmic = "-o"; } if (exists($opts{'s'})) { $uStartDate = $opts{'s'}; } if (exists($opts{'e'})) { $uEndDate = $opts{'e'}; } if (exists($opts{'S'})) { $uStep = $opts{'S'}; } if (exists($opts{'v'})) { $uVerbose = 1; } # Parameters check: # should specify multiple files ? a hostname ? a range ? data to process ? die("'$gRRDTool' is not an executable") if ! -x $gRRDTool; die("RRD Directory '$uRRDDir' is not a writeable directory") if ! -d $uRRDDir || ! -w $uRRDDir; die("Image Directory '$uImgDir' is not a writeable directory") if ! -d $uImgDir || ! -w $uImgDir; if ($uSarFile eq "") { print "sar result file not set. Please use -f option\n"; Usage(); } die("sar File '$uSarFile' is not a readable") if ! -r $uSarFile; # Added with v1.2: # Try to determine RRDTool version, because the syntax for the legend has changed between the versions: my $version = `$gRRDTool | grep Copyright`; my $dummy; ($dummy,$version) = split(/ /,$version); my @vparts = split(/\./,$version); my $gSpecialColon = 1; if ($vparts[0] < 1 || ($vparts[0] == 1 && $vparts[1] < 2)) { $gSpecialColon = 0; } # --------------------------------------------------------------------- # First pass is used to get the OS version, the count of # measures, the interval between measures and the start/end date: # --------------------------------------------------------------------- # we determine the time range: print "First Pass: determine the time range...\n"; ParseFile($uSarFile,\&HeaderCallback,\&StartTimeCallback,\&EndTimeCallback); # Sanity check: # we must have a start & endtime and curitem should not be empty: if ($gStartTime eq "") { die("Could not determine the Start Time from the output file\n"); } if ($gEndTime eq "") { die("Could not determine the End Time from the output file\n"); } #if ($gCuritem eq "") { die("No Item to graph found in the file\n"); } # Handling of graph & columns spec: if (exists($opts{'g'})) { my @graph_spec_parts = split(/:/,$opts{'g'}); if (scalar(@graph_spec_parts) < 2) { print "Incorrect syntax for '-g' option: should be '-g graphname:(+|-)column,...\n"; Usage(); } if (!exists($gSarStats->{$graph_spec_parts[0]})) { die("Value of graph specification does not start by a valid statistics name (".$graph_spec_parts[0].")\n"); } $uGraphColSign = substr($graph_spec_parts[1],0,1); if ($uGraphColSign ne "-" && $uGraphColSign ne "+") { print "Incorrect syntax for '-g' option: should be '-g graphname:(+|-)column,...\n"; Usage(); } $uGraphNameSpec = $graph_spec_parts[0]; @uGraphColSpec = split(/,/,substr($graph_spec_parts[1],1)); # Note: column names cannot be checked (or we must store predefined values somewhere ?) } # Handle start and end date specified on the command line: my $user_startsec = 0; my $user_endsec= 0; if ($uStartDate ne "") { $user_startsec = CheckDate($uStartDate,"start"); } if ($uEndDate ne "") { $user_endsec = CheckDate($uEndDate,"end"); } if ($user_startsec && $user_endsec && $user_endsec < $user_startsec) { die("The specified end date: $uEndDate, is anterior to the specified start date: $uStartDate\n"); } # Adapt the interval (in seconds) between to measures: if ($uStep) { if (0 && $uStep < $gDeltaSec) { die("The specified step ('$uStep') is less than the interval between two values ('$gDeltaSec')\n"); } else { $gDeltaSec = $uStep; } } print "Range starts from $gStartTime to $gEndTime\n"; print "Use Interval of $gDeltaSec seconds\n"; # Adjust the graph time interval: $gGraphStartSec = ($user_startsec) ? $user_startsec : $gStartSec; $gGraphEndSec = ($user_endsec) ? $user_endsec : $gEndSec; # --------------------------------------------------------------------- # Second pass: # create the graphs # --------------------------------------------------------------------- print "Second Pass: create the graphs...\n"; # Variables for graphs creation: my $gSarStat = ""; my $gSkip = 0; my $gFirstValue = 1; # skip the 1st value which is never significant with sar my $gFirstTime; my $nextblock = 0; my %graphs = (); my $dsheartbeat = 0; my $dsstring = ""; my $rras = ""; my $graphname = ""; my $dsname = ""; my $keyname = ""; my $rrdfile = ""; my $cmd = ""; my $title = ""; my $vlabel = ""; my $dsnames = ""; my $ismultiple = 0; my $startidx; my $idx; my $status; ParseFile($uSarFile,0,0,0,\&GraphHeaderCallback,\&DataCallback); # Dump the former graph: if ($gSarStat ne "") { CreateImage(); } # END sub HeaderCallback { my ($osname,$hostname) = @_; print "Analysing 'sar' output for a $osname System: $hostname\n"; $gOSName = $osname; $gHostName = $hostname; $gSarStats = $gAllSarStats{$osname}; return 0; } sub StartTimeCallback { my ($startday,$startmonth,$startyear,$starttime,$startsec,$deltasec) = @_; if ($uVerbose) { print "StartTimeCallback: startday=$startday,startmonth=$startmonth,startyear=$startyear,starttime=$starttime,startsec=$startsec,deltasec=$deltasec\n"; } $gStartTime = $starttime; $gStartSec = $startsec; $gStartDay = $startday; $gStartMonth = $startmonth; $gStartYear = $startyear; $gDeltaSec = $deltasec; return 0; } sub EndTimeCallback { my ($endday,$endmonth,$endyear,$endtime,$endsec,$mcount) = @_; if ($uVerbose) { print "EndTimeCallback: endday=$endday,endmonth=$endmonth,endyear=$endyear,endtime=$endtime,endsec=$endsec,mcount=$mcount\n"; } $gEndDay = $endday; $gEndMonth = $endmonth; $gEndYear = $endyear; $gEndTime = $endtime; $gEndSec = $endsec; $gMeasureCount = $mcount; print "$gMeasureCount measures detected\n"; return 1; # stop parsing for the 1st pass } sub GraphHeaderCallback { my ($parts) = @_; my $idx; my $columns; if ($uVerbose) { print @{$parts},"\n"; } # Make sure columns have different names: # for example, Solaris sar displays: # 11:44:20 proc-sz ov inod-sz ov file-sz ov lock-sz # here, the 1st ov will be renames proc-sz_ov, the second one inod-sz_ov my %colnames = (); for ( $idx = 1 ; $idx < scalar(@{$parts}) ; $idx++ ) { if (exists $colnames{${$parts}[$idx]}) { ${$parts}[$idx] = ${$parts}[$idx-1]."_".${$parts}[$idx]; } $colnames{${$parts}[$idx]} = 1; } # Do we need to create a new graph ? $columns = scalar(@{$parts}) - 1; my $line = join(' ',@{$parts}[1..$columns]); if ($gSarStat ne $line) { # Dump the former graph: if ($gSarStat ne "") { CreateImage(); } # Create a new graph: $dsname = MakeDSName(${$parts}[1]); if ($uGraphNameSpec ne "") { if ($dsname eq $uGraphNameSpec) { print "Analyzing data for $dsname\n"; $gSkip = 0; } else { print "Skip data for $dsname\n"; $gSarStat = $line; $gSkip = 1; next; } } else { print "Analyzing data for $dsname\n"; } # the DS Name depends on the keysize: # Sanity check: $keyname = ""; if (!exists($gSarStats->{$dsname})) { die("Unknown dsname: $dsname\n"); } if ($gSarStats->{$dsname}{'keysize'} > 1) { $keyname = MakeDSName(${$parts}[2]); $keyname = "-".$keyname; } $dsheartbeat = 2 * $gDeltaSec; $dsstring = ""; $dsnames = ""; # Is it a single or a multiple graph ? $ismultiple = $gSarStats->{$dsname}{'multiple'}; $startidx = ($ismultiple) ? 2 : 1; for ( $idx = $startidx ; $idx < scalar(@{$parts}) ; $idx++ ) { my $ds = MakeDSName(${$parts}[$idx]); if ($dsstring ne "") { $dsnames .= ":"; } $dsstring .= "DS:$ds:GAUGE:$dsheartbeat:0:U "; $dsnames .= $ds; } $rras = "RRA:AVERAGE:0.5:1:$gMeasureCount"; if (!$ismultiple) { $rrdfile = "$uRRDDir/$gHostName-$dsname$keyname.rrd"; $cmd = "$gRRDTool create $rrdfile -b $gStartSec -s $gDeltaSec $dsstring $rras"; MySystem($cmd); $gFirstValue = 1; } else { # we cannot create the RRD now, we must analyse the next lines: if ($uVerbose) { print "we must analyse more lines before creating the RRD...\n"; } $nextblock = 1; %graphs = (); } } $gSarStat = $line; return 0; } sub DataCallback { my ($cursec,$parts) = @_; my $idx; # Must skip the whole block in case of multiple graph:w if ($gFirstValue) { $gFirstValue = 0; $gFirstTime = ${$parts}[0]; return; } if (${$parts}[0] eq $gFirstTime) { return; } if ($gSkip) { next; } if ($uVerbose) { print @{$parts},"\n"; } # This is a measure line: we must update the graph my $DATA = "$cursec:"; my $startidx = ($ismultiple) ? 2 : 1; for ( $idx = $startidx ; $idx < scalar(@{$parts}) ; $idx++ ) { ${$parts}[$idx] =~ s/,/\./g; if ($idx > $startidx) { $DATA.= ":"; } # Solaris may display values such as X/Y: #11:44:20 proc-sz ov inod-sz ov file-sz ov lock-sz #11:44:50 218/30000 0 83463/128248 0 3763/3763 0 0/0 # We eliminate the /Y... (sorry !) if (${$parts}[$idx] =~ /\//) { ${$parts}[$idx] =~ s/(.*)\/.*/$1/; } $DATA .= ${$parts}[$idx]; } # Check if current line is the header for a multiple graph: if ($nextblock) { # if the column name is not in the @graphs array, add it # create a new graph when array is full: my $graphname = ${$parts}[1]; if (exists($graphs{$graphname})) { $nextblock = 0; } else { $graphs{$graphname} = 1; my $rrdfile = "$uRRDDir/$gHostName-$dsname$keyname-$graphname.rrd"; $cmd = "$gRRDTool create $rrdfile -b $gStartSec -s $gDeltaSec $dsstring $rras"; MySystem($cmd); } } # It may happen that the RRD file does not exist: # in this example, nfs2023 suddenly appears ! we ignore it: #10:07:09 nfs1 0 0.0 0 0 0.0 0.0 # nfs2 0 0.0 0 0 0.0 0.0 # nfs5 0 0.0 0 0 0.0 0.3 # nfs21 0 0.0 0 0 0.0 0.0 #10:07:39 nfs1 0 0.0 0 0 0.0 0.0 # nfs2 0 0.0 0 0 0.0 0.0 # nfs5 0 0.0 0 0 0.0 0.0 # nfs21 0 0.0 0 0 0.0 0.0 # nfs24 0 0.0 0 0 0.0 0.0 if ($ismultiple) { $rrdfile = "$uRRDDir/$gHostName-$dsname$keyname-".${$parts}[1].".rrd"; if (! -f $rrdfile) { return 0; } } my $cmd = "$gRRDTool update $rrdfile -t $dsnames $DATA"; MySystem($cmd); return 0; } # # File Parsing Encapsulation: # sub ParseFile { my ($fname,$hdrcback,$starttimecback,$endtimecback,$graphhdrcback,$datacback) = @_; my ($ST_INIT, $ST_INBLOCK, $ST_NOBLOCK) = (1..10); my $curstate = $ST_INIT; my $hostname; my $uname; my $headerline = 0; my $curheader = ""; my $inblock = 0; my $curdate; my ($startmonth,$startday,$startyear); my ($endmonth,$endday,$endyear); my $starttime = ""; my $endtime = ""; my $secondtime = ""; my $endtime_sent = 0; my @parts; my $lasttime; my $line; my $columns; # count of columns in the current block my $measurecount = 0; my $first_over = 1; my $days_over_insec = 0; open(FD,"<$fname") or die("Could not open file '$fname' in read mode\n"); while () { # Added from version 2.0c: # eliminate DOS EOL markers $_ =~ s/\r\n/\n/; chomp; # always ignore empty lines, they cannot be considered as block delimitors: if ($_ eq "") { next; } if ($uVerbose) { print "($curstate)line:$_\n"; } # Eliminate LINUX events: # 08:28:54 LINUX RESTART if ($_ =~ /LINUX/) { next; } if ($curstate == $ST_INIT) { # Handle the header: # The first line should indicate the day: # possible formats are: # for Linux: # Linux version (hostname) DD.MM.YYYY # or: Linux version (hostname) DD/MM/YYYY # or: Linux version (hostname) YYYY-MM-DD # (new formats added from version 2.0b) # (thanks to cverhoef@planet.nl) # # for SunOS (starting in v1.3): # SunOS hostname version release arch MM/DD/YY $curdate = $_; $hostname = $_; if ($curdate =~ /^Linux/i) { $curdate =~ s/^.*\(.*\)[^[:digit:]]*([[:digit:]]{2,4}[\.\/-][[:digit:]]{2}[\.\/-][[:digit:]]{2,4}).*$/$1/; $hostname =~ s/^.*\((.*)\).*$/$1/; $uname = "linux"; } elsif ($curdate =~ /^SunOS/i) { $curdate =~ s/.*([[:digit:]]{2}[\.\/][[:digit:]]{2}[\.\/][[:digit:]]{2}).*$/$1/; @parts = split(/ /,$hostname); $hostname = $parts[1]; $uname = "sunos"; } else { die("Unknown Operating System inside '$curdate'\n"); } if ($uDateFormat eq "MDY") { ($startmonth,$startday,$startyear) = split(/[\.\/-]/,$curdate); } elsif ($uDateFormat eq "DMY") { ($startday,$startmonth,$startyear) = split(/[\.\/-]/,$curdate); } # New formats added from version 2.0b elsif ($uDateFormat eq "YDM") { ($startyear,$startday,$startmonth) = split(/[\.\/-]/,$curdate); } elsif ($uDateFormat eq "YMD") { ($startyear,$startmonth,$startday) = split(/[\.\/-]/,$curdate); } else { die("Unknown Date Format: '$uDateFormat' (supported format are: MDY, DMY, YDM and YMD)\n"); } ($endday,$endmonth,$endyear) = ($startday,$startmonth,$startyear); $startmonth--; if ($hdrcback) { if ($hdrcback->($uname,$hostname)) { last; } } $curstate = $ST_NOBLOCK; next; } # Starting from here, we know the header has been analysed: # Multiple spaces are considered as a simple char: $_ =~ s/([[:space:]]+)/ /g; #print $_,"\n"; @parts = split(/[[:space:]]/); if ($curstate == $ST_NOBLOCK) { # We expect a block header: BLOCKHDR: # Lines must start with a time specification: #Average nfs1 0 0.0 0 0 0.0 0.0 if ($parts[0] !~ /[[:digit:]]{2}:[[:digit:]]{2}:[[:digit:]]/) { next; } $curstate = $ST_INBLOCK; StripAMPM(\@parts); # Always memorize the last time: # since version 2.0c, this assignment is made AFTER StripAMPM() call ! $lasttime = $parts[0]; # Indicate a new graph may start here: $curheader = $parts[1]; $columns = scalar(@parts); if ($graphhdrcback) { if ($graphhdrcback->(\@parts)) { last; } } next; # the starttime starts AFTER the header line } if ($curstate == $ST_INBLOCK) { StripAMPM(\@parts); # Ignore repeated headers such as : #11:30:01 PM proc/s #11:40:01 PM 5.04 #11:50:01 PM 5.04 # #03:50:01 AM proc/s #04:10:01 AM 18.05 #04:20:01 AM 17.18 if ($parts[1] eq $curheader) { next; } # Sometimes the time may be missing (Solaris...): #09:55:39 device %busy avque r+w/s blks/s avwait avserv # #09:56:09 nfs1 0 0.0 0 0 0.0 0.0 # nfs2 0 0.0 0 0 0.0 0.0 # nfs5 0 0.0 0 2 0.0 3.7 if ($parts[0] =~ /^[[:space:]]/ || !length($parts[0])) { # put the last time back: $parts[0] = $lasttime; #unshift(@parts,$lasttime); } # Determine the end of block: empty lines are enough on Linux, # not on solaris: # on Solaris, empty lines may not delimit blocks: # blocks end with a line such as "^Average....", depending on the locale. # Sometimes the new block starts with a new header line... # #09:55:39 proc-sz ov inod-sz ov file-sz ov lock-sz #09:56:09 132/30000 0 91719/128248 0 1895/1895 0 0/0 #16:35:39 127/30000 0 60543/128248 0 1881/1881 0 0/0 # #09:55:39 msg/s sema/s #09:56:09 0.00 0.10 #09:56:39 0.00 0.00 # we consider the end block when # the line starts with a non-digit and non-space char # or when the first column changed and is not a number and not a DS Name ! my $isdsname = MakeDSName($parts[1]); if ($parts[0] !~ /^[[:digit:][:space:]]/ || ($parts[1] !~ /^[0-9]/ && exists($gSarStats->{$isdsname}))) { if ($uVerbose) { print "End of block\n"; } # Note: all block is supposed to end with an "Average" line if (!$endtime_sent) { #print $parts[0]," ",$parts[1]," ",$lasttime,"\n"; die; $endtime_sent = 1; $endmonth--; my $endsec = timelocal(reverse(split(/:/,$endtime)),$endday,$endmonth,$endyear); if ($endtimecback) { if ($endtimecback->($endday,$endmonth,$endyear,$endtime,$endsec,$measurecount)) { last; } } $endmonth++; } $curstate = $ST_NOBLOCK; $days_over_insec = 0; $starttime = $secondtime = $endtime = $curheader = ""; $first_over = 1; if ($parts[1] !~ /^[0-9]/ && exists($gSarStats->{$isdsname})) { goto BLOCKHDR; } next; } # Always memorize the last time: $lasttime = $parts[0]; # Skip or add missing parameters to incomplete lines (see Solaris: #11:44:20 runq-sz %runocc swpq-sz %swpocc #11:44:50 2.5 7 #11:45:20 if (scalar(@parts) < 2) { next; } my $idx = scalar(@parts); for ( ; $idx < $columns ; $idx++ ) { $parts[$idx] = ""; } # Is it the first row of real value ? # we must memorize the starting date: if ($starttime eq "") { $starttime = $parts[0]; } # If it is the second row of value, we get the date # to compute the interval between two measures: elsif ($secondtime eq "" && $starttime ne $parts[0]) { # Note: we suppose the first and second lines belong to the same day ! $secondtime = $parts[0]; my $startsec = timelocal(reverse(split(/:/,$starttime)),$startday,$startmonth,$startyear); my $secondsec = timelocal(reverse(split(/:/,$secondtime)),$startday,$startmonth,$startyear); my $deltasec = $secondsec - $startsec; if ($starttimecback) { if ($starttimecback->($startday,$startmonth,$startyear,$starttime,$startsec,$deltasec)) { last; } } } # We also need to compute the measure count: # Ignore lines which have identical timestamp (think of multiple CPU !) if ($endtime eq "" || $endtime ne $parts[0]) { $measurecount++; } # cross over midnight ? if ($starttime gt $parts[0]) { if ($first_over) { print "cross over($endyear,$endmonth,$endday)\n"; eval { ($endyear,$endmonth,$endday) = Add_Delta_Days($endyear,$endmonth,$endday,1); $days_over_insec += 86400; }; if ($@) { die("Incorrect Date Format on the 1st line\nUse the -t option\n"); } $first_over = 0; } } else { $first_over = 1; } $endtime = $parts[0]; # This is a measure line: if ($datacback) { my $cursec = timelocal(reverse(split(/:/,$parts[0])),$gStartDay,$gStartMonth,$gStartYear); $cursec += $days_over_insec; if ($datacback->($cursec,\@parts)) { last; } } next; } } close(FD); } sub CheckDate { my ($date,$label) = @_; my @date; my $sec; my $month; @date = split(/[ :-]/,$date); $sec = timelocal($date[5],$date[4],$date[3],$date[1],$date[0]-1,$date[2]); #print "sec=$sec, startsec=$startsec, endsec=$endsec\n"; if ($sec < $gStartSec) { $month = $gStartMonth+1; die("The $label date specified on the command line: $date, is anterior to the first date read in the file: $month-$gStartDay-$gStartYear $gStartTime\n"); } if ($sec > $gEndSec) { $month = $gEndMonth+1; die("The $label date specified on the command line: $date, is posterior to the last date read in the file: $month-$gEndDay-$gEndYear $gEndTime\n"); } return $sec; } sub MakeDSName { my ($name) = @_; $name =~ s/%/prct_/g; $name =~ s/[^[:alnum:]]/_/g; return $name; } sub Time24 { my ($parts) = @_; # Add 12 hours, but 12PM is 12 and 12AM is 00: #print "p0=",${$parts}[0],"--"; my @thetime = split(/:/,${$parts}[0]); if (${$parts}[1] eq "AM") { if ($thetime[0] eq "12") { ${$parts}[0] = "00:".$thetime[1].":".$thetime[2]; } } else { if ($thetime[0] ne "12") { ${$parts}[0] = $thetime[0]+12; ${$parts}[0] .= ":".$thetime[1].":".$thetime[2]; } } if ($uVerbose) { print "Time24: new time is: ",${$parts}[0],"\n"; } } sub StripAMPM { my ($p) = @_; # Eliminate the 2nd column if it is a AM or a PM: #12:00:01 AM proc/s #12:10:01 AM 17.15 if ($p->[1] eq "AM" || $p->[1] eq "PM") { Time24($p); # Eliminate the column: my $part0 = $p->[0]; shift(@{$p}); shift(@{$p}); unshift(@{$p},$part0); } } sub CreateImage { my $title; my $vlabel; if ($gSkip) { return; } $title = $gSarStats->{$dsname}{'title'}; $vlabel = $gSarStats->{$dsname}{'unit'}; # $dsnames is col1:col2:.... # we must apply $uGraphColSign and $uGraphColSpec here: my @ds = split(/:/,$dsnames); my $defs = ""; my $color; my $imgfile; COL: for ( my $idx = 0 ; $idx < scalar(@ds) ; $idx++ ) { if ($uGraphNameSpec ne "") { my $colname = $ds[$idx]; if ($uGraphColSign eq "+") { # the column must be listed my $found = 0; foreach my $col (@uGraphColSpec) { if ($col eq $colname) { $found = 1; last; } } if (!$found) { next COL; } } else { # the column must not be listed foreach my $col (@uGraphColSpec) { if ($col eq $colname) { next COL; } } } } $color = $gColors[$idx % scalar(@gColors)]; if ($ismultiple) { $defs .= "DEF:v$idx=RRDFILE:".$ds[$idx].":AVERAGE LINE$gLineWidth:v$idx#$color:".$ds[$idx]." "; } else { $defs .= "DEF:v$idx=$rrdfile:".$ds[$idx].":AVERAGE LINE$gLineWidth:v$idx#$color:".$ds[$idx]." "; } } if ($defs eq "") { die("No column selected to display the graph\n"); } my $startdate = localtime($gGraphStartSec); my $enddate = localtime($gGraphEndSec); if ($gSpecialColon) { $startdate =~ s/:/\\:/g; $enddate =~ s/:/\\:/g; } #print $startdate,"\n"; #print $enddate,"\n"; my $legend = '"COMMENT:From '.$startdate.', To '.$enddate.'\\c" "COMMENT:\\n"'; if ($ismultiple) { foreach $graphname (keys %graphs) { $imgfile = "$uImgDir/$gHostName-$dsname$keyname-$graphname.png"; # set the good RRD file name: my $defs2 = $defs; $defs2 =~ s!RRDFILE!$uRRDDir/$gHostName-$dsname$keyname-$graphname.rrd!g; if ($keyname ne "") { my $keyname2 = substr($keyname,1); # suppress the leading '-' $title = $gSarStats->{$dsname}{'title'}{$keyname2}." for $graphname"; } else { $title = $gSarStats->{$dsname}{'title'}." $graphname"; } $cmd = "$gRRDTool graph $imgfile -t '$title' -s $gGraphStartSec -e $gGraphEndSec $uLogarithmic -S $gDeltaSec -v '$vlabel' -w $uImgWidth -h $uImgHeight -a PNG $legend $defs2 >/dev/null"; MySystem($cmd); } } else { $imgfile = "$uImgDir/$gHostName-$dsname$keyname.png"; $cmd = "$gRRDTool graph $imgfile -t '$title' -s $gGraphStartSec -e $gGraphEndSec $uLogarithmic -S $gDeltaSec -v '$vlabel' -w $uImgWidth -h $uImgHeight -a PNG $legend $defs >/dev/null"; MySystem($cmd); } } sub MySystem { my ($cmd) = @_; my $status; if ($uVerbose) { print $cmd,"\n"; } if ($status = system($cmd)) { if (!$uContinueOnErrors) { die("Command '$cmd' failed with return code: $status\n"); } } } sub Usage { print "Usage: $0\t[-?ovc] [-d rrd_dir] [-i img_dir] [-W width] [-H height]\n"; print "\t\t\t[-s start_date] [-e end_date] [-S step]\n"; print "\t\t\t[-g graph_spec] [-t DMY|MDY|YDM|YMD] -f sar_file\n"; print "Options:\n"; print "\t-? : this help\n"; print "\t-v : verbose mode\n"; print "\t-c : continue on error\n"; print "\t-o : use a logarithmic scale for Y scale\n"; print "\t-d rrd_dir : directory where RRD files must be created\n"; print "\t-i img_dir : directory where to place PNG images\n"; print "\t-W width : images width (in pixels)\n"; print "\t-H height : images height (in pixels)\n"; print "\t-s start_date : start date (MM-DD-YYYY HH:MM:SS)\n"; print "\t-e end_date : end date (MM-DD-YYYY HH:MM:SS)\n"; print "\t-S step : interval (in seconds) between to values in the graph\n"; print "\t-g graph_spec: by default all possible graphs are created\n"; print "\t\tgraph_spec syntax is: data[:(+|-)column[,column...]]\n"; print "\t\tthis creates only the graph and the specified columns\n"; print "\t\tnote that graph and column name must contain '_' instead of '-'\n"; print "\t-t MDY|DMY|YDM|YMD: indicates the format for the date displayed on the 1st output line\n"; print "\t-f sar_file : file to analyse - create by the 'sar -f ...' command\n"; exit(1); } exit(0); # EOF